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:深入理解sizeofstrlen的差异

sizeof是一个编译时确定的运算符,可以用于获取变量或类型在内存中占用的字节数。它在编译阶段就已确定,不依赖于运行时数据。

相对地,strlen是一个运行时确定的库函数,专门用于计算以空字符\0结尾的字符串的实际字符数。由于它需要遍历字符串,因此其结果仅在运行时才可知。

面试题 4:解析C与C++中static关键字的不同用途

在C语言中,static用于修饰局部静态变量(延长生命周期至程序结束)、外部静态变量(限制链接至其他文件)和静态函数(限制函数的作用域至定义它的文件内)。

在C++中,static除了上述功能外,还用于类中定义静态成员变量和静态成员函数。静态成员属于整个类,而非单个对象,常用于计数器或共享数据的存储。

面试题 5:比较C语言的malloc与C++的new

mallocfree是C标准库函数,用于动态内存的分配与释放。malloc分配内存但不初始化,free仅释放内存。

newdelete是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, 5a作为数组名,代表数组首地址;&a取地址操作后,再强制类型转换为int*,指向数组之后的内存位置。

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

C/C++程序内存分配包括:

  1. 静态存储区:存储全局变量、静态变量、常量。
  2. 栈区:存储函数局部变量、函数参数。
  3. 堆区:通过malloc/new分配,由程序员管理。

面试题 10:区分strcpysprintfmemcpy

  • strcpy用于字符串复制。
  • sprintf用于格式化输出到字符串。
  • memcpy用于内存块复制,不仅限于字符串。

面试题 11:设置特定地址的整型变量值

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;

这个例子展示了如何通过强制类型转换将整型数据转换为指针,并设置其值。

面试题 12:面向对象的三大特征

  • 封装性:数据和方法的保护。
  • 继承性:代码重用和扩展。
  • 多态性:接口的统一和实现的多样性。

面试题 13:探讨C++中的空类及其成员函数

在C++中,一个空类默认包含以下成员函数:

  • 缺省构造函数:自动生成,用于创建类的新实例。
  • 缺省拷贝构造函数:在对象之间进行浅拷贝。
  • 缺省析构函数:在对象生命周期结束时自动调用。
  • 缺省赋值运算符:用于对象间的赋值操作。
  • 缺省取址运算符:允许获取对象的地址。
  • 缺省取址运算符 const:常量版本的取址运算符,保证对象不会被修改。

值得注意的是,这些成员函数仅在实际使用时才会由编译器定义。此外,深入理解这些函数的默认行为对于优化类设计至关重要。

面试题 14:拷贝构造函数与赋值运算符的深入分析

拷贝构造函数和赋值运算符在类的操作中扮演着不同角色:

  1. 拷贝构造函数:用于生成新的类对象实例,不需要检查源对象与目标对象是否相同,因为它总是创建新实例。
  2. 赋值运算符:用于将一个对象的状态复制到另一个已经存在的对象。在赋值前,需要检查自赋值,并妥善处理内存释放等问题。

特别地,当类包含指针成员时,为了管理内存,避免内存泄漏,通常需要重写这两个函数,而不是依赖编译器提供的默认实现。

面试题 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:链表与数组的比较

链表和数组在数据结构中有以下区别:

  1. 存储形式:数组使用连续内存空间,链表使用非连续的动态内存空间。
  2. 数据查找:数组支持快速查找,链表需要顺序检索。
  3. 数据插入或删除:链表支持快速的插入和删除操作,数组可能需要大量数据移动。
  4. 越界问题:链表没有越界问题,数组存在越界风险。

选择合适的数据结构取决于具体需求。

面试题 20:单链表反序的实现

单链表反序可以通过以下两种方法实现:

  1. 循环算法
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;
}
  1. 递归算法
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++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。

全部评论

相关推荐

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