27、必考 | C/C++ 最常见50道面试题
@[toc]
面试题 1:深入探讨变量的声明与定义的区别
在编程中,变量的声明指的是告知编译器变量的名称和类型,但不分配内存空间。声明可以多次,常见于头文件中,用于模块间的接口声明。使用extern
关键字声明的变量,意味着其定义在别处,通常在另一个文件中。
相对地,定义则是创建一个具有存储空间的变量实例。定义只能有一次,通常在源文件中,确保为变量分配内存空间。例如,全局变量和局部变量的定义就是分配内存并初始化的过程。
面试题 2:编写比较“零值”的if
语句
在JavaScript中,对基本数据类型与“零值”的比较可以通过以下if
语句实现:
// 对于布尔型数据:
if (flag) {
// A: 执行当flag为true时的操作
} else {
// B: 执行当flag为false时的操作
}
// 对于整数型数据:
if (0 !== flag) {
// A: 执行当flag非零时的操作
} else {
// B: 执行当flag为零时的操作
}
// 对于指针型数据:
if (NULL === flag) {
// A: 执行当flag为空指针时的操作
} else {
// B: 执行当flag非空指针时的操作
}
// 对于浮点型数据:
if ((flag >= -NORM) && (flag <= NORM)) {
// A: 执行当flag在正常范围内时的操作
} else {
// B: 执行当flag超出正常范围时的操作
}
注意,为避免潜在的赋值错误,应将“零值”置于比较操作的左侧。
面试题 3:深入理解sizeof
与strlen
的差异
sizeof
是一个编译时确定的运算符,可以用于获取变量或类型在内存中占用的字节数。它在编译阶段就已确定,不依赖于运行时数据。
相对地,strlen
是一个运行时确定的库函数,专门用于计算以空字符\0
结尾的字符串的实际字符数。由于它需要遍历字符串,因此其结果仅在运行时才可知。
面试题 4:解析C与C++中static
关键字的不同用途
在C语言中,static
用于修饰局部静态变量(延长生命周期至程序结束)、外部静态变量(限制链接至其他文件)和静态函数(限制函数的作用域至定义它的文件内)。
在C++中,static
除了上述功能外,还用于类中定义静态成员变量和静态成员函数。静态成员属于整个类,而非单个对象,常用于计数器或共享数据的存储。
面试题 5:比较C语言的malloc
与C++的new
malloc
和free
是C标准库函数,用于动态内存的分配与释放。malloc
分配内存但不初始化,free
仅释放内存。
new
和delete
是C++操作符,用于对象的动态创建与销毁。new
分配并初始化内存,delete
释放内存并调用析构函数。new
返回具体类型的指针,而malloc
返回void
指针。
面试题 6:实现一个“标准”的MIN
宏
#define MIN(a, b) ((a) <= (b) ? (a) : (b))
使用时应注意宏的副作用,特别是在复杂的表达式中,可能会因宏展开导致意外行为。
面试题 7:指针是否可以是volatile
是的,指针可以是volatile
,这表明指针指向的值可能会在程序的控制之外改变,如在中断服务程序中。
面试题 8:探讨a
和&a
的区别
#include <stdio.h>
int main() {
int a[] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d, %d", *(a + 1), *(ptr - 1));
return 0;
}
输出结果为2, 5
。a
作为数组名,代表数组首地址;&a
取地址操作后,再强制类型转换为int*
,指向数组之后的内存位置。
面试题 9:详述C/C++程序编译时的内存分配
C/C++程序内存分配包括:
- 静态存储区:存储全局变量、静态变量、常量。
- 栈区:存储函数局部变量、函数参数。
- 堆区:通过
malloc
/new
分配,由程序员管理。
面试题 10:区分strcpy
、sprintf
与memcpy
strcpy
用于字符串复制。sprintf
用于格式化输出到字符串。memcpy
用于内存块复制,不仅限于字符串。
面试题 11:设置特定地址的整型变量值
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
这个例子展示了如何通过强制类型转换将整型数据转换为指针,并设置其值。
面试题 12:面向对象的三大特征
- 封装性:数据和方法的保护。
- 继承性:代码重用和扩展。
- 多态性:接口的统一和实现的多样性。
面试题 13:探讨C++中的空类及其成员函数
在C++中,一个空类默认包含以下成员函数:
- 缺省构造函数:自动生成,用于创建类的新实例。
- 缺省拷贝构造函数:在对象之间进行浅拷贝。
- 缺省析构函数:在对象生命周期结束时自动调用。
- 缺省赋值运算符:用于对象间的赋值操作。
- 缺省取址运算符:允许获取对象的地址。
- 缺省取址运算符 const:常量版本的取址运算符,保证对象不会被修改。
值得注意的是,这些成员函数仅在实际使用时才会由编译器定义。此外,深入理解这些函数的默认行为对于优化类设计至关重要。
面试题 14:拷贝构造函数与赋值运算符的深入分析
拷贝构造函数和赋值运算符在类的操作中扮演着不同角色:
- 拷贝构造函数:用于生成新的类对象实例,不需要检查源对象与目标对象是否相同,因为它总是创建新实例。
- 赋值运算符:用于将一个对象的状态复制到另一个已经存在的对象。在赋值前,需要检查自赋值,并妥善处理内存释放等问题。
特别地,当类包含指针成员时,为了管理内存,避免内存泄漏,通常需要重写这两个函数,而不是依赖编译器提供的默认实现。
面试题 15:设计一个不允许继承的C++类
以下是一个使用模板和友元声明来阻止类继承的C++类示例:
template <typename T> class A {
friend T; // 允许T访问私有成员
private:
A() {}
~A() {}
};
class B : virtual public A<B> {
public:
B() {}
~B() {}
};
class C : virtual public B {
public:
C() {}
~C() {}
};
int main() {
B b; // C c; // 这将导致编译错误
return 0;
}
通过将构造函数和析构函数声明为私有,可以阻止类被继承。这种设计模式在需要控制类使用场景时非常有用。
面试题 16:访问基类的私有虚函数
以下程序展示了如何通过特定技巧调用基类的私有虚函数:
#include <iostream>
class A {
public:
virtual void g() {
std::cout << "A::g" << std::endl;
}
private:
virtual void f() {
std::cout << "A::f" << std::endl;
}
};
class B : public A {
public:
void g() {
std::cout << "B::g" << std::endl;
}
virtual void h() {
std::cout << "B::h" << std::endl;
}
};
typedef void (*Fun)();
void main() {
B b;
Fun pFun;
for (int i = 0; i < 3; i++) {
pFun = (Fun)*((int*)*((int*)&b) + i);
pFun();
}
}
输出结果为:
B::g
A::f
B::h
这个示例展示了虚函数表的工作原理和多态性的重要性。
面试题 17:类成员函数的重写、重载和隐藏的区别
- 重写:发生在派生类与基类之间,要求基类函数必须有
virtual
修饰符,参数列表必须一致。 - 重载:发生在同一个类中,参数列表必须不同,与
virtual
修饰符无关。 - 隐藏:发生在派生类与基类之间,参数列表可以相同也可以不同,但函数名必须相同。如果参数不同,即使基类函数有
virtual
修饰,也会发生隐藏而非重写。
重载和覆盖是实现多态性的基础,但它们的技术实现和目的完全不同。
面试题 18:多态实现的原理
多态的实现依赖于虚函数表(vtable)和虚函数指针(vptr)。当类中存在虚函数时,编译器会为此类生成vtable,并在构造函数中将vptr指向相应的vtable。这样,通过this
指针就可以访问到正确的vtable,实现动态绑定和多态。
面试题 19:链表与数组的比较
链表和数组在数据结构中有以下区别:
- 存储形式:数组使用连续内存空间,链表使用非连续的动态内存空间。
- 数据查找:数组支持快速查找,链表需要顺序检索。
- 数据插入或删除:链表支持快速的插入和删除操作,数组可能需要大量数据移动。
- 越界问题:链表没有越界问题,数组存在越界风险。
选择合适的数据结构取决于具体需求。
面试题 20:单链表反序的实现
单链表反序可以通过以下两种方法实现:
- 循环算法:
List reverse(List n) {
if (!n) return n;
List cur = n.next, pre = n, tmp;
pre.next = null;
while (cur != null) {
tmp = cur;
cur = cur.next;
tmp.next = pre;
pre = tmp;
}
return pre;
}
- 递归算法:
List* reverse(List* oldList, List* newHead = NULL) {
if (oldList == NULL) return newHead;
List* next = oldList->next;
oldList->next = newHead;
newHead = oldList;
return (next == NULL) ? newHead : reverse(next, newHead);
}
循环算法直观易懂,递归算法则需要对循环算法有深刻理解。
面试题 21:深入分析队列和栈的异同及其内存分配
队列和栈作为两种基本的线性数据结构,在数据处理流程中扮演着重要角色。它们的主要区别在于数据的存取原则:队列遵循“先进先出”(FIFO)原则,而栈则采用“后进先出”(LIFO)原则。这种差异导致它们在实际应用场景中的使用方式也不尽相同。
在内存管理方面,需要区分程序内存中的“栈区”和“堆区”。栈区由编译器自动管理,用于存储函数调用时的局部变量和参数,其存取方式与数据结构中的栈相似。相对地,堆区的内存分配和释放通常由程序员控制,如果程序员不释放,可能需要等到程序结束时由操作系统回收。堆的内存分配方式与链表类似,但与数据结构中的“堆”不同。
面试题 22:实现队列功能的经典栈操作
通过两个栈实现队列功能是一种经典的数据结构应用。以下是使用C语言实现的示例代码,展示了如何通过两个栈进行队列操作:
// 节点结构体定义
typedef struct node {
int data;
struct node *next;
} node, *LinkStack;
// 创建空栈
LinkStack CreateNULLStack(LinkStack *S) {
*S = (LinkStack)malloc(sizeof(node));
if (*S == NULL) {
printf("Failed to malloc a new node.\n");
return NULL;
}
(*S)->data = 0;
(*S)->next = NULL;
return *S;
}
// 栈的插入函数
LinkStack Push(LinkStack *S, int data) {
if (*S == NULL) {
printf("No node in stack!\n");
return *S;
}
LinkStack p = (LinkStack)malloc(sizeof(node));
if (p == NULL) {
printf("Failed to malloc a new node.\n");
return *S;
}
p->data = data;
p->next = (*S)->next;
(*S)->next = p;
return *S;
}
// 出栈函数
node Pop(LinkStack *S) {
node temp = {0, NULL};
if (*S == NULL) {
printf("No node in stack!\n");
return temp;
}
LinkStack p = (*S)->next;
node n = *p;
(*S)->next = p->next;
free(p);
return n;
}
// 双栈实现队列的入队函数
void StackToQueuePush(LinkStack *S, int data) {
LinkStack S1 = NULL;
CreateNULLStack(&S1);
node n;
while ((*S)->next != NULL) {
n = Pop(S);
Push(&S1, n.data);
}
Push(&S1, data);
while (S1->next != NULL) {
n = Pop(&S1);
Push(S, n.data);
}
}
这段代码展示了如何使用两个栈实现队列的基本操作,包括入队和出队。
面试题 23:计算二叉树的深度
二叉树的深度是衡量树结构复杂度的重要指标。以下是一个使用递归方法计算二叉树深度的示例代码:
// 定义二叉树节点结构
typedef struct BiTNode {
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 计算二叉树的深度
int depth(BiTree T) {
if (T == NULL) return 0;
int d1 = depth(T->lchild);
int d2 = depth(T->rchild);
return (d1 > d2 ? d1 : d2) + 1;
}
这段代码通过递归调用自身来计算左右子树的深度,并返回较大的深度值加一。
面试题 24:直接插入排序的实现
直接插入排序是一种简单直观的排序算法,它通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。以下是直接插入排序的实现代码:
#include <iostream>
using namespace std;
void InsertionSort(int ARRAY[], int length) {
for (int i = 1; i < length; i++) {
int key = ARRAY[i];
int j = i - 1;
while (j >= 0 && ARRAY[j] > key) {
ARRAY[j + 1] = ARRAY[j];
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。