C++面试题目汇总

参考链接:https://mp.weixin.qq.com/s/RD2Hm9L22VV6CSWNvCWhdQ

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 进行修饰。在 C++ 中,volatile 是一个类型修饰符(type qualifier),用于告诉编译器:该变量的值可能会在程序不知情的情况下被改变。因此,编译器不能对该变量进行某些优化(如缓存、重排等)。

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的区别

表达式类型含义
aint*指向数组首元素 a[0] 的指针
&aint (*)[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++ 设计一个不能被继承的类?

✅ 方法一:使用 final 关键字(推荐,C++11 及以上)

从 C++11 开始,你可以使用关键字 final 来标记一个类为“最终类”,表示它不能被继承

class Base final {
public:
    void sayHello() {
        std::cout << "Hello from Base!" << std::endl;
    }
};

// 编译错误!无法继承 final 类
class Derived : public Base { };  // ❌ Error: cannot derive from 'final' class 'Base'

✅ 方法二:私有构造函数 + 静态工厂方法(适用于 C++98/03)

如果你使用的是 C++98 或 C++03,没有 final 关键字,可以通过将基类的构造函数设为私有,并提供静态工厂方法来创建对象,从而阻止继承。

原理:

  • 派生类在构造时必须调用基类的构造函数;
  • 如果基类的构造函数是私有的,派生类就无法访问;
  • 因此无法完成继承。
class SealedClass {
private:
    SealedClass() {}  // 私有构造函数

public:
    static SealedClass createInstance() {
        return SealedClass();
    }

    void doSomething() {
        std::cout << "Doing something..." << std::endl;
    }
};

// 编译错误!无法访问基类的私有构造函数
class Derived : public SealedClass {};  // ❌ Error: cannot access private constructor

✅ 方法三:使用友元和空类(更高级技巧)

还有一种更巧妙的方式是利用模板和友元机制,让只有特定类才能继承自己。这种技术常用于实现“密封类”。

//使用 CRTP 技术
template <typename T>
class MakeSealed {
    friend T;
private:
    MakeSealed() {}
};

class SealedClass : public MakeSealed<SealedClass> {
public:
    void sayHi() {
        std::cout << "Hi!" << std::endl;
    }
};

// 编译错误!无法访问基类的私有构造函数
class Derived : public SealedClass {};  // ❌ Error

//解释:
//MakeSealed<T> 的构造函数是私有的;
//只有 T 是它的友元,所以只有 T 能访问构造函数;
//所以只有 SealedClass 能继承 MakeSealed<SealedClass>;
//其他类(如 Derived)无法访问构造函数,也就无法继承。

✅ 总结对比

方法是否标准支持是否推荐说明
final 关键字✅ C++11+✅ 推荐最简单、最直观
私有构造函数✅ C++98+⚠️ 可选适用于旧版本
模板 + 友元✅ C++98+⚠️ 高级技巧更灵活但复杂

✅ 实际建议

  • 如果你使用的是 C++11 或更新版本,请直接使用 final 关键字;
  • 如果你维护的是 旧项目(C++98/03),可以考虑使用私有构造函数或模板友元方式;
  • 在现代 C++ 中,final 是最清晰、最安全的方式。

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:简述类成员函数的重写、重载和隐藏的区别

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

  • 作用范围不同: 重写的函数位于两个不同的类中(基类和派生类),而重载的函数始终在同一个类中。

  • 参数列表不同: 重写函数与被重写函数的参数列表必须完全相同;而重载函数与被重载函数的参数列表必须不同(包括参数个数、类型或顺序)。

  • virtual 关键字要求不同: 基类中的被重写函数必须使用 virtual 关键字进行修饰,才能实现多态;而重载函数无论是否使用 virtual 都不影响其重载特性。

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

  • 作用范围不同: 与重写类似,隐藏也发生在基类与派生类之间,而不是像重载那样在同一类中。

  • 参数列表不同: 隐藏函数与被隐藏函数的参数列表可以相同,也可以不同,但它们的函数名必须相同。当参数列表不同时,不论基类中的函数是否为虚函数,都会导致基类函数被隐藏,而不是被重写。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值