【c++面试总结】

1. NULL 和 nullptr 区别

int overLoadTest(int x) {
    cout << __LINE__ << endl;

    return 0;
}

int overLoadTest(char* x) {
    cout << __LINE__ << endl;

    return 0;
}

int main() {
    char x[10] = {1,2,3,4,5};
    overLoadTest(1);
    overLoadTest(x);
    overLoadTest(nullptr);
    // overLoadTest(NULL); // ambiguous

    return 0;
}

在这里插入图片描述

  • 结论:
    1. NULL 的定义 实际是 0;在重载函数中传入 NULL 具有二义性,编译器无法对 0 和 空指针类型区分
    1. nullptr 是c++11引入的安全的空指针字面量,不会被隐式转换为其他非指针类型,将去除二义性,可以安全的赋值给指针类型。

2. static 作用

  1. static 修饰变量或函数改变该变量的作用域和生命周期。使其仅在该文件中可见,有效隐藏了static变量,可以避免命名冲突、降低代码耦合度。
    static函数存储在代码段
    static变量存储在全局数据段
  2. 底层实现
    符号本地化:
    ` 编译:编译器生成的符号具有内部链接属性,只在当前编译单元可见
    · 链接:链接器不会将static符号输出到其他模块,也不会将其他模块同名函数输入。
    符号解析:
    · 链接器在链接阶段会解析符号引用,对于static符号,只会查找当前模块内的定义,不会从其他模块寻找。
    · 如果其他文件尝试引用static类型符号,链接器会报为解析符号引用,因为该符号不会出现在其他模块符号表中。

3. free()函数只传入一个地址,为什么知道释放空间大小

malloc分配的内存为一个个chunk,定义如下:

struct malloc_chunk {    
	size_t prev_size;  /* 前一个内存块的大小(如果合并的话) */    
	size_t size;       /* 当前内存块的大小,包括边界标记 */    
	struct malloc_chunk *fd;  /* 指向前一个空闲内存块的指针(用于空闲内存列表) */    
	struct malloc_chunk *bk;  /* 指向下一个空闲内存块的指针(用于空闲内存列表) */};

在这里插入图片描述
图片来自wx公众号“CppPlayer”
malloc分配内存后返回的指针不是指向Header,而是指向Payload的起始位置,所占用的空间大小记录在参数指针指向地址的前面,所以知道需要释放内存的大小

几个小知识:
malloc不是系统调用,通过 brk()从堆区分配内存 或 mmap()在文件映射区域分配虚拟内存。
只有当程序首次访问这片地址,操作系统通过MMU将虚拟地址转为物理地址,更新页表完成映射;如果这片内存实际没有使用过,不会分配实际的物理空间,节约内存。
调用free,内存不会马上被操作系统回收。为了减少与操作系统内存交互次数,降低系统开销。
首先会被内存管理器使用双链表等方式保存起来,有助于减少内存碎片和提高内存使用效率。

4. main()函数执行前还会执行什么代码

  1. 初始化全局变量
  2. 执行 全局对象 的构造函数
  3. 类内部声明的 静态成员对象 属于整个类而不是类的实例,因此提前初始化
  4. c运行时库初始化通常包括 设置缓冲区和初始化堆。c++可能进行初始化操作:设置运行环境、配置标准输入输出…

5. 堆和栈的数据访问速度

栈通常比堆块。
栈的数据存储在连续的内存单元,访问速度快;堆的数据存储在分散的存储空间,需要额外的指针解引用操作

栈特点:
自动管理:内存分配和回收由编译器自动处理
连续内存:栈内存一般连续,有助于优化cpu缓存
快速分配和回收:栈管理简单,分配和回收速度快
固定大小:栈大小通常在程序运行前就已经决定,超出后会发生栈溢出。
寻址方式:直接寻址

堆特点:
动态分配:显示分配
非连续内存:堆分配内存一般不连续,因为堆可以被分成多个部分,每部分可以独立回收和分配(内存碎片产生原因)
寻址方式:间接寻址

6. 构造函数不能为虚函数

  1. 从代码层面看
    虚函数表中存储了一个类所有虚函数的地址,它允许通过基类的指针调用派生类函数,实现运行时多态。
    虚表存储在只读数据段,虚函数表地址在编译时被确定,并链接到程序的二进制文件。
    当一个类被创建时,才会有虚表指针指向虚函数表,对象在没有初始化完成的时候不知道虚表位置,自然不能访问虚表内容
  2. 从设计角度看
    虚表的存在主要是为了解决 编译期间没办法确定具体调用对象的问题,但c++在编译期间就能确定要创建的对象具体类型,没有意义
  3. 使用角度看
    虚函数主要用于父类调用,构造函数主要自己调用

除了构造函数还有哪些函数不能是虚函数?
1. 普通函数(不属于类的实例)
2. 内联函数(内联函数作用是将函数调用替换为函数体代码,和虚函数运行时动态绑定冲突)
3. 静态成员函数(不属于类的实例)
4. 友元函数(不属于类的实例)
5. 模板函数中的成员函数(模板中的成员函数默认内联)
6. 私有成员函数(没有使用意义)

7. 结构体可以直接赋值吗

声明时可以直接初始化,同一结构体的不同对象直接也可以直接赋值;
之前项目中清空结构体内容的方式就是赋空结构体值(无指针情况)
手撕c++string 类

class String {
    char* m_data;   // 字符串 
public:
    String(const char*str = nullptr);   // 普通构造函数
    String(const String& other);        // 拷贝构造函数
    ~String();                          // 析构函数
    String & operator =(const String& other);   // 赋值构造函数
    String  operator +(const String& other) const;   // 重载运算符+
    friend std::ostream& operator<<(std::ostream& os, const String& s);   // 重载运算符<<
};

String::String(const char*str) {
    if(str == nullptr) {
        m_data = new char[1];
        *m_data = '\0';
    } else {
        int len = strlen(str);
        m_data = new char[len + 1];
        strcpy_s(m_data, len + 1, str);
    }
}

String::String(const String& other) {
    int len = strlen(other.m_data);
    m_data = new char[len + 1];
    strcpy_s(m_data, len + 1, other.m_data);
}

String::~String() {
    delete m_data;
}

String & String::operator =(const String& other) {
    // 检查自赋值
    if(this == &other) {
        return *this;
    }
    delete[]m_data;
    int len = strlen(other.m_data);
    m_data = new char[len + 1];
    strcpy_s(m_data, len+1, other.m_data);
    return *this;
}

String String::operator+(const String& other) const{
    if(other.m_data == '\0') {
        return String(other);
    }
    int len = strlen(other.m_data);
    int thisLen = strlen(m_data);
    char* tmp = new char[len + thisLen + 1];
    strcpy_s(tmp, thisLen + 1, m_data);
    strcpy_s(tmp + thisLen, len + 1, other.m_data);
    String s(tmp);
    delete tmp;
    return s;
}

std::ostream& operator<<(std::ostream& os, const String& s) {
    os << s.m_data;
    return os;
}

int main() {
    String s("123");
    String s1(s);
    String s2 = s1;
    String s3 = s + s1 + s2;

    cout << s << " ";
    cout << s1 << " ";
    cout << s2 << " ";
    cout << s3 << endl;

    return 0;
}

在这里插入图片描述

8. explicit的作用

class Example {
public:
    explicit Example(int x) {
        cout << "Constructor called with" << x << endl;
    }
};

void fun(Example e){
    cout << "function suc" << endl;

    return;
}

int main() {
    fun(42); // error: could not convert '42' from 'int' to 'Example'

    return 0;
}

如果不加 explicit ,这段程序是可以正常运行的
C++ 允许使用单参数构造函数来进行隐式类型转换
fun()会将 ‘42’ 隐式转换成Example类型
在这里插入图片描述
加上后就会出错,不能进行隐式转换
作用:防止隐式转换的发生,避免产生一些意外行为。
如下代码定义了类型转换函数operator int(),允许将对象直接转换成int类型,但如果加了 explicit修饰,此时隐式转换失效,必须使用显示转换static_cast;

class Example {
    int value_;
public:
    // explicit Example(int x) {
    Example(int x):value_(x) {
        cout << "Constructor called with " << x << endl;
    }
    explicit operator int() {
        return value_;
    }
};

void fun(Example e){
    cout << "function suc" << endl;

    return;
}

int main() {
    fun(42);
    Example obj(42);
    // int intValue = obj; // 不存在从 "Example" 到 "int" 的适当转换函数
    int intValue = static_cast<int>(obj);
    cout << intValue << endl;
    return 0;
}

9. 类型转换函数

1. static_cast

主要用于编译时静态类型转换
适用场景:

  • 基本数据类型之间的转换(int、double)
  • 指针类型的上行转换(即从派生类指针转换为基类指针)
  • 枚举类型转换为整数
  • 可以用于任何不会改变对象底层二进制表示的转换

注意事项:

不能用于不同类型的指针之间(如 int* 转 double*)。
无法在运行时捕捉到转换是否失败,因此转换过程是基于类型信息静态分析

2. dynamic_cast

用于运行时的动态类型转换,会在运行时执行类型检查,因此它只能用于带有虚函数的多态类型。主要用于设涉及继承层次的指针和引用间的转换
适用场景:

  • 用于基类指针(引用)向派生类指针(引用)的转换(即下行转换)
  • 运行时安全地判断是否可以进行这种转换,若转换失败则返回 nullptr(对于指针)或抛出异常(对于引用)。

注意事项:

  • 仅对带有虚函数的多态类有效。
  • 运行时开销较大,因为需要检查类型安全性。
  • 仅适用于指针或引用之间的转换。
class Base {
    virtual void fun() {} // 必须多态类
};

class Derived : public Base {
public:
    void derived_fun() {
        cout << "Derived class function called." << endl;
    }
};

class OtherClass : public Base{
};

void static_cast_test() {
    Base* basePtr = new Derived;
    Derived* derivedPtr = static_cast<Derived*>(basePtr);
    if(derivedPtr){
        derivedPtr->derived_fun();
    }

    delete basePtr;
}

void dynamic_cast_test() {
    Base* basePtr = new Derived;
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if(derivedPtr) {
        derivedPtr->derived_fun();
    } else {
        cout << "dynamic_cast failed." << endl;
    }

    delete derivedPtr;

    Base* otherBasePtr = new OtherClass;
    Derived* failedCast = dynamic_cast<Derived*>(otherBasePtr);
    if(!failedCast) {
        cout << "dynamic_cast failed as expected" << endl;
    }
    failedCast = static_cast<Derived*>(otherBasePtr);
    if(failedCast) {
        cout << "static_cast suc" << endl;
        failedCast->derived_fun();
    } else {
        cout << "static_cast failed." << endl;
    }

    delete otherBasePtr;

    return;
}

int main() {
    static_cast_test();
    dynamic_cast_test();

    return 0;
}

在这里插入图片描述
在chatgpt的帮助下学习static_cast和dynamic_cast的区别,又是被ai教育的一天
分析一下:
定义了三个类,其中Derived类、OtherClass都继承自Base类,且相互独立。
static_cast_test()主要展示了static_cast用法,其实一般用于上行转换,这里用了下行,也可以,编译器不会进行类型检查,只要类型在继承层次中存在关系就可以转换,默认我知道自己在做什么,然而,如果指针的实际类型不是派生类,使用 static_cast 进行类型转换会导致未定义行为。
dynamic_cast_test()使用了dynamic_cast进行类型转换,并在失败时输出错误信息,这里展示了dynamic可以用于安全的下行转换,并且在类型不匹配时(failedCast)返回nullptr,避免未定义行为。
此时再用static_cast对otherClass进行转换,发现可以正确输出信息,但OtherClass和Derived之间没有继承关系,编译器没有发现这个错误,仍然调用了fun函数,但此时发生的是未定义行为,并不能保证在所有环境下都会这样工作
未定义行为意味着程序可以表现出任何不确定的结果,通常情况下:

  • 程序可能会崩溃。
  • 可能会输出错误的或不可预期的结果。
  • 甚至可能会成功输出正确的结果(但这只是表象,实际行为是不安全的)。

总结

  • 代码中 static_cast 强制转换了不相关类型(OtherClass 转换为 Derived),尽管编译通过并且运行后输出了结果,但这属于未定义行为,不应被依赖。
  • 输出 “static_cast suc” 和 “Derived class function called” 只是未定义行为的一种表现,不能保证程序在不同环境下的行为是一致的。在实际生产代码中,这样的代码会被认为是不安全的,可能导致崩溃或潜在的漏洞。
  • dynamic_cast 在这种场景下是更安全的选择,因为它会在运行时进行检查,确保 Base* 确实指向 Derived 对象。

3. const_cast

const_cast一般用于移除(添加)对象的 const 和 volatile 属性,也是唯一可以修改对象 const 和 volatile 属性的类型转换
一般用于c++与旧的c语言API兼容。如下

void C_API_fun(char* str) {};

void Cpp_API_fun(const char* str) {
	C_API_fun(const_cast<char*>(str)); // // 调用不支持 const 的旧 C API
}

主要用途

  • 移除(添加)对象的 const 限定符。
  • 移除(添加)对象的 volatile 限定符。

但是,const_cast 只能对指针或引用进行类型转换,不能直接改变普通变量的 const 性质,否则产生未定义行为。

void const_cast_test() {
    int x = 10;
    const int* ptr = &x;
    // *ptr = 20; // error: 表达式必须是可修改的左值C/C++(137)
    int* modifiablePtr = const_cast<int*>(ptr);
    *modifiablePtr = 20; // 移除对象const属性,修改普通对象值

    const int y = 10;
    modifiablePtr = const_cast<int*>(&y);
    *modifiablePtr = 20; // 未定义行为,不允许修改const对象

    return;
}

4. reinterpret_cast

通常用于在不同类型之间进行底层表示的强制转换。与其他类型转换运算符相比,reinterpret_cast 并不会执行类型检查或类型安全转换,它仅仅是将对象的位模式重新解释为另一种类型。

目的是允许程序员对指针、整数和不同类型的指针之间进行低级别的强制转换,但这种转换可能会带来风险,因为它不保证类型安全。

void reinterpret_cast_test() {
    // 1. 指针与整数间的转换,以便进行位操作或存储地址信息
    int i = 1;
    uintptr_t intPtr = reinterpret_cast<uintptr_t>(&i);
    int* ptr = reinterpret_cast<int*>(intPtr);
    *ptr = 2;

    // 2. 将对象的位模式重新解释为不同类型,在对象序列化、反序列化或二进制数据处理时使用
    float f = 3.14f;
    int* p = reinterpret_cast<int*>(&f);
    cout << "Bit pattern: " << *p << endl;

    // 3. 指针类型间的转换,底层编程
    int i = 42;
    void* p = reinterpret_cast<void*>(&i);
    int* ip = reinterpret_cast<int*>(p);

    // 4. 兼容旧代码: reinterpret_cast 常用于与低级硬件或系统交互的代码中,
    //                或者与遗留代码进行兼容时进行特殊的类型转换

    return;
}

reinterpert_cast和static_cast很像,只不过static_cast多了静态类型检查,例如 两个毫无关联的类对象指针在做转换时,static_cast会报错,但reinterpret_cast不会检查,可以正常转换,有安全风险。
在这里插入图片描述

10. 智能指针

本来打算在c++11新特性总结的,但既然四种类型转换都写了,就刚好把智能指针也写写。
智能指针是c++11引入的新特性,用于自动化内存管理,避免手动管理动态分配的内存,防止内存泄漏和悬空指针等问题。智能指针通过RAII机制,确保对象在超出作用域时自动调用析构函数,释放资源。
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种资源管理的编程惯用法,旨在确保资源的正确分配和释放。
核心思想:将资源的分配和释放绑定到对象的生命周期上。
RAII的优势之一是:确保即使发生异常,也可以保证资源被正确释放。例如:函数中抛出异常,对象仍会在销毁前调用析构函数释放资源。可以有效地避免因异常导致的资源泄露。
常见应用
智能指针、ifstream文件管理、lock_guard…

10.1 shared_ptr

用于共享所有权的智能指针,允许多个智能指针指向同一个动态分配的对象,并在最后一个shared_ptr 销毁时,自动释放内存资源。
shared_ptr实现依赖于 “control block”机制该机制负责引用计数管理和资源释放

10.1.1 基础结构

分为两部分:

  • 裸指针:指向所管理的动态对象
  • 控制块:包括资源的引用计数删除器
class control_block {
private:
    int shared_count;  // 强引用计数
    int weak_count;    // 弱引用计数
    void* resource;    // 指向资源的指针
    std::function<void(void*)> deleter;  // 删除器(可以是默认的或自定义的)
    ...
}
10.1.2 make_shared不能使用自定义删除器

通过 make_shared 创建智能指针提供了一种更高效的内存分配方式——将control block和实际对象合并到同一内存块进行分配,可以减少内存碎片;这种策略将控制器和对象的生命周期绑定在一起,因此,内存的分配和释放策略也统一。同时,控制块没有足够的空间存储一个自定义删除其对象;因此在销毁时,shared_ptr使用默认删除器销毁。

class Test {
    int age_;
public:
    Test(int age) : age_(age) {}
    ~Test() {}
    void displsy() {
        cout << "age is " << age_ << endl;
    }
};

void shared_ptr_test() {
    shared_ptr<Test> ptr1 = make_shared<Test>(10);
    ptr1->displsy();
    cout <<  ptr1.use_count() << endl;

    shared_ptr<Test> ptr2 = ptr1;
    ptr1->displsy();
    cout << ptr1.use_count() << " ";
    cout << ptr2.use_count() << endl;

    ptr1.reset();
    // ptr1->displsy(); // ptr1已销毁
    ptr2->displsy();
    cout << ptr1.use_count() << " ";
    cout << ptr2.use_count() << endl;

    return;
}

在这里插入图片描述

10.1.3 循环引用

为什么产生循环引用
shared_ptr 的引用计数机制:当两个或多个对象 shared_ptr 相互引用时,引用计数不会降为 0,导致对象无法销毁

void circular_reference() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();

    a->ptr_B = b;
    b->ptr_A = a;

    cout << "Before reset: " << endl;
    cout << "a.use_count() = " << a.use_count() << " ";
    cout << "b.use_count() = " << b.use_count() << endl;

    // 调用 reset() 试图手动释放 a 和 b
    a.reset();
    b.reset();
    cout << "After reset: " << endl;
    // reset() 后 a 和 b 的引用计数依然为 1,循环引用仍然存在
    if (a) cout << "a.use_count() = " << a.use_count() << endl;
    if (b) cout << "b.use_count() = " << b.use_count() << endl;

    // 在函数结束时,引用计数不会归零,A 和 B 都不会被销毁
    return;
}

在这里插入图片描述
函数结束时,并没有调用析构函数释放内存;循环引用的问题确实存在。
当函数结束时,a和b的局部变量被清空,但此时a->pre_A仍然持有 b 的shared_ptr,此时 b 的引用计数减一后依然为1,同理 b->ptr_B持有a的shared_ptr, a 的引用计数也不会为0,资源无法释放

2. weak_ptr

引入 weak_ptr主要为了解决shared_ptr循环引用问题
weak_ptr不会增加强引用计数,当外部的shared_ptr销毁时,a和b的引用计数都会置为减为0,从而保证资源正确释放
weak_ptr本身不能直接访问管理的对象。为了访问weak_ptr指向的对象,必须使用 weak_ptr.lock()函数将其转换为 shared_ptr。如果源对象已经销毁,返回空,避免了悬空指针

为什么 weak_ptr 不能直接访问所管理的对象?

weak_ptr 主要作用是观察由 shared_ptr 所管理的对象,weak_ptr 不改变强引用计数,因此不能保证所观察的对象是否仍然存在,例如

void weak_ptr_test() {
    shared_ptr<int> sptr = make_shared<int>(10);
    weak_ptr<int> wptr = sptr;

    sptr.reset();   // 释放 shared_ptr, 引用计数归 0, 对象销毁

    // wptr仍然存在,但指向的对象已经被销毁
    if(shared_ptr<int> sptr2 = wptr.lock()) {
        cout << *sptr2 << endl; // 安全访问对象
    } else {
        cout << "sptr destroy" << endl;
    }

    return;
}

在这里插入图片描述

3. unique_ptr

相比于 shared_ptr 可以共享对象所有权,unique_ptr 保证在任何时刻,都只有一个指针拥有对象的所有权。当指针被销毁、转移、置空时,对象也会被自动销毁
使用场景:

  • 适用于资源离开作用域被自动释放场景,如:文件句柄、网络连接、动态内存等…
  • 不可共享的独占资源,避免错误的多次释放
  • 避免裸指针,使用 unique_ptr 可以避免手动管理裸指针的复杂性和潜在的内存泄露风险
3.1 主要特性
  • 轻量级——没有额外的引用计数等级制,比shared_ptr更轻量
  • 独占所有权——无法赋值,只能通过转移所有权来移动
  • 自动释放内存——当unique_ptr退出作用域或显示被reset、release时,自动销毁所管理的对象
3.2 实现原理
  • 禁止拷贝
    unique_ptr禁止了拷贝构造和拷贝赋值操作
		unique_ptr(const unique_ptr&) = delete;
		unique_ptr& operator=(const unique_ptr&) = delete;
  • 移动语义
    可以通过移动语义转移所有权
	unique_ptr(unique_ptr&& u) noexcept; // 移动构造
	unique_ptr& operator=(unique_ptr&& u) noexcept; // 移动赋值

其他智能指针
auto_ptr: c++11被unique_ptr取代(赋值时所有权转移,而不是引用计数管理)
boost::scoped_ptr:类似unique_ptr,但不可转移;适用于简单的内存管理场景,没有所有权转移需求
boost::instrusive_ptr:允许自定义引用计数管理,适合高效管理自带引用计数的对象。
boost::shared_array:允许多个 shared_ptr 动态管理一个数组,引用计数为0释放。

11. 左值引用和右值引用

11.1 左值引用

左值引用是 C++ 中最早引入的引用类型。它指向左值(Lvalue),即有明确存储位置(可以取地址)并且持久存在的对象。左值是那些可以在多次操作中持续存在的变量,比如函数作用域内的局部变量、全局变量等。
特点

  • 左值引用必须引用左值(可以是一个变量,也可以是返回左值的表达式)。
  • 左值引用用于别名机制,可以通过引用名来操作原对象。
  • 左值引用是可以修改引用对象的值的。
    应用场景
  • 函数参数传递:传递对象引用,避免对象拷贝
11.2 右值引用

右值引用是在 C++11 中引入的,它指向右值(Rvalue)。右值是那些没有持久地址、只能被临时使用的对象,通常是表达式的结果,如字面常量、临时对象、返回右值的函数调用等。
特点:

  • 右值引用只能绑定到右值,右值通常是临时对象、字面量或临时表达式结果。
  • 右值引用允许修改右值(通常右值是不可修改的)。
  • 右值引用的主要应用是移动语义,它允许对临时对象进行资源转移,而不是资源复制,从而提高程序性能。
    应用场景
  • 移动语义
	std::string str1 = "Hello";
	std::string str2 = std::move(str1);  // 将 str1 的资源转移到 str2
  • 完美转发
void foo(int& x) {
    cout << "foo called with lvalue" << endl;
    return;
}

void foo(int&& x) {
    cout << "foo called with rvalue" << endl;
    return;
}

template<typename T>
// 引用折叠规则确定 T&& 实际类型
void wrapper(T&& arg) {
    // 完美转发,保持arg的左值或右值特性
    foo(std::forward<T>(arg));
}

int main() {
    int a = 10;
    wrapper(a);

    wrapper(10);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值