C++ 面试常问问题(一)

本文详细解析了C++面试中常见的几个问题,包括初始化列表的使用、继承与析构函数的调用顺序、多态下的析构问题,以及字符串sizeof的细节。通过示例代码解释了构造函数和析构函数的执行流程,强调了虚析构函数在多态中的重要性,并探讨了指针和数组在sizeof运算中的差异。此外,还涉及了bool和浮点数与0比较的注意事项。

这篇文章讲解C++ 面试常问的几个问题。本文通过demo讲解初始化列表,继承,字符串等常问问题。看下边这个例子

初始化列表

//基类
class Base
{
public:
    Base() : m_nbase(0), m_nbase2(m_nbase + 100) {}
    Base(int n) : m_nbase(n), m_nbase2(m_nbase + 100)
    {
        cout << "this is Base construct " << endl;
    }
    ~Base()
    {
        cout << "this is Base destruct " << endl;
    }

   virtual void printData()
    {
        cout << "this is Base printData " << endl;
        cout << "data is " << m_nbase << endl;
        // cout << "base2 data is " << m_nbase2 << endl;
    }

    void printData2()
    {
        cout << "base2 data is " << m_nbase2 << endl;
    }

    int SizeOf(char p[])
    {
        return sizeof(p);
    }

    int SizeOf2(char *p)
    {
        return sizeof(p);
    }

private:
    int m_nbase;
    int m_nbase2;
};

实现了一个类Base,类的构造函数采用了初始化列表,初始化列表按顺序初始化初始类成员。
接下来在main函数里调用如下

Base b1(1);
b1.printData();
b1.printData2();

1 问Base的初始化列表是否会报错?
回答:
不会有问题,因为初始化列表按顺序初始化类成员。所以会分别输出m_nbase的值为1, m_nbase2的值为101

继承问题

继承问题常问到的是基类和子类的关系,继承先构造基类再构造子类。析构时先析构子类,再析构基类。
我们实现一个Derive类继承自Base

class Derive : public Base
{
public:
    Derive(int n) : Base(n), m_nderive(n) 
    { cout << "this is Derive construct " << endl; }

     ~Derive()
    {
        cout << "this is Derive destruct " << endl;
    }

    void printData()
    {
        cout << "this is Derive printData" << endl;
    }

private:
    int m_nderive;
};

Derive类继承了Base类,接下来我们在主函数中调用如下

Derive d1(2);
d1.printData();
cout << " ................." << endl;

1 问 上面程序输出什么
答 输出如下

this is Base construct 
this is Derive construct 
this is Derive printData
this is Derive destruct 
this is Base destruct 

因为构造时,先构造基类再构造子类,析构时先析构子类再析构基类
如果在主函数中调用如下

Base *b1 = new Derive(2);
b1->printData();
delete b1;

2 问上面程序输出什么?
答输出如下

this is Base construct 
this is Derive construct 
this is Derive printData
this is Base destruct 

因为构造时同样先构造基类,再构造子类。
由于创建子类对象返回基类指针,调用虚函数printData会触发多态机制,调用了子类的printData。
析构时由于析构函数不是虚函数,所以不会调用子类的析构函数!!!
所以只会输出Base的析构函数。
3 问如何解决多态情况下子类无法析构问题?
答 将基类和子类的析构函数都设置为虚析构函数即可,少一个都不行!!!
修改如下

class Base
{
public:
    Base() : m_nbase(0), m_nbase2(m_nbase + 100) {}
    Base(int n) : m_nbase(n), m_nbase2(m_nbase + 100)
    {
        cout << "this is Base construct " << endl;
    }
    ~Base()
    {
        cout << "this is Base destruct " << endl;
    }

   virtual void printData()
    {
        cout << "this is Base printData " << endl;
        cout << "data is " << m_nbase << endl;
        // cout << "base2 data is " << m_nbase2 << endl;
    }
    //....省略
};

class Derive : public Base
{
public:
    Derive(int n) : Base(n), m_nderive(n) 
    { cout << "this is Derive construct " << endl; }

    virtual  ~Derive()
    {
        cout << "this is Derive destruct " << endl;
    }

    void printData()
    {
        cout << "this is Derive printData" << endl;
    }

private:
    int m_nderive;
};

此时再调用

Base *b1 = new Derive(2);
b1->printData();
delete b1;

程序输出

this is Base construct 
this is Derive construct 
this is Derive printData
this is Derive destruct 
this is Base destruct 

字符串sizeof

字符串的sizeof也是常问的问题

char carry[100] = "Hello World";
cout << "sizeof(carry)  " << sizeof(carry) << endl;
char *pstr = "Hello World";
cout << "sizeof(pstr)   " << sizeof(pstr) << endl;
cout << "sizeof(*pstr)  " << sizeof(*pstr) << endl;
Base b1;
cout << "b1.SizeOf(carry)  " << b1.SizeOf(carry) << endl;
cout << "b1.SizeOf2(carry) " << b1.SizeOf2(carry) << endl;

1 问上边程序输出什么
答 输出如下

sizeof(carry)  100
sizeof(pstr)  4
sizeof(*pstr)  1
b1.SizeOf(carry) 4
b1.SizeOf2(carry) 4

carry是个数组,所以sizeof(carry)是数组的大小为100
pstr是个char指针,所以sizeof(pstr)是指针大小为4字节,当然在64位机器为8
我的机器上输出的是8,这个根据机器不同而不同,记住是指针大小就行。
pstr指向字符串首地址,也是第一个字符的地址,*pstr为第一个字符
sizeof(*pstr)为一个字符的大小,所以为1
b1.SizeOf以及b1.SizeOf2内部调用的都是sizeof()一个char指针,所以都为4,
我的机器上输出为8

bool,float和0比较

bool和0比较

bool bval = true;
if(!bval){
    //...
    //bval为0的逻辑
}else{
    //...
}

float和0比较

    float f1 = 0.1;
    if (f1 <= FLT_EPSILON && f1 >= -FLT_EPSILON)
    {
        cout << "this is float 0" << endl;
    }
    else
    {
        cout << "this is not float 0" << endl;
    }

    double d1 = 0.0;
    if (d1 <= DBL_EPSILON && d1 >= -DBL_EPSILON)
    {
        cout << "this is double 0" << endl;
    }
    else
    {
        cout << "this is not double 0" << endl;
    }

总结

本文介绍了一些面试常见问题
源码链接
https://gitee.com/secondtonone1/cpplearn
想系统学习更多C++知识,可点击下方链接。
C++基础

<think>在C++面试中,常被问到的八股问题包括但不限于以下几个方面: 1. 面向对象编程(OOP)的特性:封装、继承、多态 2. 内存管理:new/delete, malloc/free, 内存泄漏,智能指针 3. 模板与泛型编程 4. 异常处理 5. STL(标准模板库)的使用和原理 6. 多线程与并发 7. 虚函数、虚表、虚指针 8. const关键字的使用 9. 引用与指针的区别 10. 拷贝控制:拷贝构造函数、拷贝赋值运算符、移动语义等 下面我将对其中些重点问题进行详细解答。 ### 1. 面向对象编程的三大特性 **封装**:将数据和操作数据的方法绑定在起,并对外部隐藏实现细节。通过访问修饰符(public, protected, private)实现。 **继承**:允许个类(派生类)继承另个类(基类)的成员,并可以添加自己的成员。支持多继承(C++中)。 **多态**:允许不同类的对象对同消息作出响应。分为编译时多态(函数重载、运算符重载、模板)和运行时多态(通过虚函数和继承实现)。 ### 2. 多态的实现(运行时多态) 运行时多态通过虚函数和继承实现。基类中声明虚函数,派生类可以重写这些函数。当通过基类指针或引用调用虚函数时,会根据实际对象的类型调用相应的函数。 ```cpp class Base { public: virtual void show() { cout << "Base show\n"; } virtual ~Base() {} // 虚析构函数 }; class Derived : public Base { public: void show() override { cout << "Derived show\n"; } }; int main() { Base* b = new Derived(); b->show(); // 输出 "Derived show" delete b; return 0; } ``` **解释**:基类Base中的show()是虚函数,派生类Derived重写了show()。通过基类指针b(实际指向Derived对象)调用show()时,会调用Derived的show()。 ### 3. 虚函数表(vtable)和虚指针(vptr) 每个含有虚函数的类都有个虚函数表,其中存放了虚函数的地址。每个对象有个虚指针(vptr)指向该表。当调用虚函数时,通过vptr找到vtable,再根据函数在表中的位置调用正确的函数。 ### 4. 智能指针 用于自动管理动态内存,避免内存泄漏。C++11引入: - `unique_ptr`:独占所有权,不能拷贝,只能移动。 - `shared_ptr`:共享所有权,通过引用计数管理。 - `weak_ptr`:不增加引用计数,用于打破`shared_ptr`的循环引用。 ### 5. 移动语义和右值引用 C++11引入移动语义,避免不必要的拷贝,提高性能。通过右值引用(`&&`)和移动构造函数、移动赋值运算符实现。 ```cpp class MyString { public: // 移动构造函数 MyString(MyString&& other) noexcept { data = other.data; size = other.size; other.data = nullptr; // 避免重复释放 other.size = 0; } // 移动赋值运算符 MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] data; data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } private: char* data; size_t size; }; ``` ### 6. const关键字 - 修饰变量:表示常量,不可修改。 - 修饰指针:`const int* p`(指向常量的指针)或`int* const p`(指针本身是常量)。 - 修饰成员函数:表示该函数不会修改对象的状态(除了mutable成员)。 ### 7. 引用与指针的区别 - 引用是别名,必须初始化,不能为空,不能改变引用的对象;指针是变量,可以改变指向。 - 引用使用起来更安全,但指针更灵活(如支持指针运算)。 ### 8. 模板 模板是泛型编程的基础,分为函数模板和类模板。 ```cpp template <typename T> T max(T a, T b) { return a > b ? a : b; } template <typename T> class Stack { // 栈的实现 }; ``` ### 9. STL容器 常用容器: - 序列容器:vector, list, deque, array - 关联容器:set, map, multiset, multimap - 无序关联容器:unordered_set, unordered_map ### 10. 多线程 C++11引入线程库。常用组件: - `std::thread`:创建线程。 - `std::mutex`:互斥量,用于同步。 - `std::condition_variable`:条件变量。 - `std::future`和`std::promise`:用于异步操作。 以上是C++面试中常见的问题和解答。由于篇幅限制,每个问题只能简要介绍。在实际面试中,可能会深入追问细节。 §§相关问题§§: 1. 什么是虚函数表(vtable)?它是如何工作的? 2. 智能指针有哪几种?它们的使用场景和区别是什么? 3. 什么是移动语义?它如何提高程序性能? 4. 请解释const成员函数的作用,以及mutable关键字的作用。</think>### C++面试常问八股问题解析 #### 1. **虚函数(Virtual Function)** **问题**:虚函数的作用和实现原理? **解答**: 虚函数实现运行时多态,允许派生类重写基类方法。通过虚函数表(vtable)实现: - 每个含虚函数的类有个 vtable,存储虚函数地址 - 对象包含指向 vtable 的指针(vptr) - 调用时通过 vptr 找到实际函数地址 ```cpp class Base { public: virtual void show() { std::cout << "Base\n"; } // 虚函数 }; class Derived : public Base { public: void show() override { std::cout << "Derived\n"; } // 重写虚函数 }; int main() { Base* obj = new Derived(); obj->show(); // 输出 "Derived" (运行时多态) delete obj; } ``` #### 2. **智能指针(Smart Pointers)** **问题**:`unique_ptr`、`shared_ptr`、`weak_ptr` 的区别? **解答**: - `unique_ptr`:独占所有权,不可复制,可移动 - `shared_ptr`:共享所有权,基于引用计数 - `weak_ptr`:解决循环引用,不增加引用计数 ```cpp // unique_ptr 示例 std::unique_ptr<int> uptr = std::make_unique<int>(10); // shared_ptr 示例 auto sptr1 = std::make_shared<int>(20); auto sptr2 = sptr1; // 引用计数+1 // weak_ptr 示例 std::weak_ptr<int> wptr = sptr1; if (auto tmp = wptr.lock()) { // 安全访问 std::cout << *tmp; } ``` #### 3. **移动语义(Move Semantics)** **问题**:右值引用和移动构造函数的用途? **解答**: 避免深拷贝开销,直接"窃取"临时对象资源: - 右值引用用 `&&` 标识 - `std::move` 将左值转为右值 - 移动构造函数/赋值运算符转移资源所有权 ```cpp class String { char* data; public: // 移动构造函数 String(String&& other) noexcept : data(other.data) { other.data = nullptr; // 防止重复释放 } // 移动赋值运算符 String& operator=(String&& other) noexcept { if (this != &other) { delete[] data; data = other.data; other.data = nullptr; } return *this; } }; ``` #### 4. **const 关键字** **问题**:const 在函数声明中的不同位置有何含义? **解答**: - `const T* ptr`:指向常量的指针(内容不可改) - `T* const ptr`:常量指针(指向不可改) - `void func() const`:成员函数不修改对象状态 ```cpp class MyClass { int value; public: // const 成员函数:承诺不修改成员变量 int getValue() const { return value; } // 参数为指向常量的指针 void print(const int* ptr) const { // *ptr = 10; // 错误:不能修改常量内容 std::cout << *ptr; } }; ``` #### 5. **模板元编程(Template Metaprogramming)** **问题**:如何用模板实现编译时计算? **解答**: 利用模板特化和递归在编译期生成代码: ```cpp // 编译期计算阶乘 template <int N> struct Factorial { static const int value = N * Factorial<N-1>::value; }; template <> struct Factorial<0> { static const int value = 1; }; int main() { std::cout << Factorial<5>::value; // 输出 120 (编译时计算) } ``` #### 6. **虚析构函数(Virtual Destructor)** **问题**:为什么基类析构函数需要声明为 virtual? **解答**: 确保通过基类指针删除派生类对象时,调用完整的析构链: ```cpp class Base { public: virtual ~Base() { std::cout << "Base dtor\n"; } // 虚析构函数 }; class Derived : public Base { public: ~Derived() override { std::cout << "Derived dtor\n"; } }; int main() { Base* obj = new Derived(); delete obj; // 正确调用 Derived::~Derived() 和 Base::~Base() } ``` **未声明虚析构函数的后果**: 仅调用基类析构函数,导致派生类资源泄漏。 #### 7. **STL 容器底层实现** **问题**:vector/list/map 的底层数据结构和特性? **解答**: | 容器 | 数据结构 | 特性 | |--------|--------------|----------------------------------| | vector | 动态数组 | 随机访问O(1),尾部插入O(1) | | list | 双向链表 | 插入/删除O(1),不支持随机访问 | | map | 红黑树 | 键值有序,查找/插入O(log n) | | unordered_map | 哈希表 | 平均O(1)查找,无序存储 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恋恋风辰

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值