C/C++高频面经-秋招篇

自己在秋招找工作过程中遇到的一些C/C++面试题,大中小厂都有,分享出来,希望能帮到有缘人。

C语言

snprintf()的使用

函数原型为int snprintf(char *str, size_t size, const char *format, …)
两点注意:
(1) 如果格式化后的字符串长度 < size,则将此字符串全部复制到str中,并给其后添加一个字符串结束符(‘\0’);
(2) 如果格式化后的字符串长度 >= size,则只将其中的(size-1)个字符复制到str中,并给其后添加一个字符串结束符(‘\0’),返回值为欲写入的字符串长度。

snprintf的返回值是欲写入的字符串长度abcdefghijk,而不是实际写入的字符串度。如:

char buf[8];
int n = snprintf(buf, 5, "abcdefghijk");
printf("n %d    buf %s\n", n, buf);
 
运行结果为:
n 11     buf abcd

注意这个结果,只输出了abcd,长度为4,不是期待的5,没有输出 e ,说明snprintf函数最后自动加上去的’\0’,是算在size内部的,是格式化字符串的总长度(不包括’\0’),这里使用sizeof(buf)时需要注意+1,这一点与malloc申请空间类似。

总结:

1.snprintf会自动在格式化后的字符串尾添加\0,结尾符是包含在size长度内部的。
2.snprintf会在结尾加上\0,不管buf空间够不够用,所以不必担心缓冲区溢出。
3.snprintf的返回值n,当调用失败时,n为负数,当调用成功时,n为格式化的字符串的总长度(不包括\0),当然这个字符串有可能被截断,因为buf的长度不够放下整个字符串。

可以判断输出
if ( n < 0) : snprintf出错了
if ( n >0 && n < sizeof(buf) ) : snprintf成功,并且格式了完成的字符串。
if ( n >= sizeof(buf) ) : snprintf成功,但要格式化的字符串被截断了。

C语言实现多态

没有内置的机制来直接实现类和多态。可以使用结构体和函数指针来模拟类和多态的概念。具体步骤如下:

  1. 定义结构体作为类:
typedef struct {
    // 类中的成员变量

    // 类中的虚函数指针
    void (*polymorphicFunction)();
} Base;
  1. 定义派生结构体:
typedef struct {
    Base base;  // 继承基类

    // 派生结构体中的成员变量

} Derived;
  1. 定义函数指针来实现多态:
void derivedPolymorphicFunction() {
      // 派生结构体中重写的虚函数
}

// 创建派生结构体对象
Derived derivedObj;

// 将函数指针指向派生结构体的重写函数
base.polymorphicFunction = derivedPolymorphicFunction;

// 通过基类指针调用虚函数,实现多态
base.polymorphicFunction();

模拟memmove

用于在内存中移动一段数据,它会处理源地址和目标地址重叠的情况

#include <stddef.h>

void *memmove(void *dest, const void *src, size_t n) {
    unsigned char *d = dest;
    const unsigned char *s = src;

    if (d == s) {
        // Nothing to do if source and destination are the same
        return dest;
    } 
    else if(d < s || d >= (s + n)) {
        // Non-overlapping memory, safe to copy from start to end
        while (n--) {
            *d++ = *s++;
        }
    }
    else {
        // Overlapping memory, copy from end to start
        d += n;
        s += n;
        while (n--) {
            *(--d) = *(--s);
        }
    }

    return dest;
}

模拟memcpy

在内存之间进行字节级别的复制

void my_memcpy(void* dest, const void* src, size_t size) {
    unsigned char* d = (unsigned char*)dest;
    const unsigned char* s = (const unsigned char*)src;
    
    for (size_t i = 0; i < size; i++) {
        d[i] = s[i];
    }
}

模拟strcmp

#include <stdio.h>

int my_strcmp(const char* str1, const char* str2) {
    while (*str1 != '\0' || *str2 != '\0') {
        if (*str1 != *str2) {
            return (*str1 - *str2);
        }
        str1++;
        str2++;
    }
    return 0; // 字符串相等
}

int main() {
    const char* str1 = "Hello";
    const char* str2 = "Hello";
    int result = my_strcmp(str1, str2);

    if (result == 0) {
        printf("Strings are equal.\n");
    } else if (result < 0) {
        printf("str1 is less than str2.\n");
    } else {
        printf("str1 is greater than str2.\n");
    }

    return 0;
}

C函数函数调用的过程?用到了哪些栈指针?

1)调用者保存当前函数的上下文
2)传递参数
3) 跳转到被调用的函数
4)创建栈帧:保存局部变量,参数,返回地址等信息
5)执行函数体
6)返回
7)恢复调用者的上下文
栈指针(通常是寄存器,如ESP或RSP)用于管理栈上的数据

C有哪些操作字符串的函数?

strcpy:比较2个字符串
strlen:算字符串长度
strcat:拼接字符串
strcmp:字符串的比较
strncpy:可以指定比较的字符
strstr:在一个字符串中查找另一个字符串第一次出现
strchr: 在一个字符串中查找某个字符的第一次出现

struct和union的区别

struct里面的成员在内存中各自占有独立的空间,大小=所有成员的大小之和,可以同时访问结构体中的多个成员,用于多个相关的数据字段,比如人:姓名,年龄,性别
union 里面的成员是共用内存的,大小=最大成员的大小,用于节约内存,一个数据包:文本/图像/音频,只需要存储其中一种数据类型

struct和class的区别

struct默认公有,类成员可以外部访问,默认继承是公有
class默认私有,类内访问,或者使用公有接口,默认继承也是私有
都可以定义友元关系,外部类或函数可以访问私有成员

struct内存对齐

减少内存碎片,CPU更加高效的访问成员
1)结构体的对齐要求是其成员中最大的数据类型的对齐要求,比如int和char,按照int对齐
2)结构体的大小必须是其对齐要求的整数倍

内存管理

什么是内存对齐

内存对齐(Memory Alignment)是指在内存中分配变量时,将其存储在地址是特定值的倍数上的要求。这是由于硬件的要求,以提高内存读取和写入操作的效率。
在计算机系统中,数据的读取和写入通常是以内存块为单位进行的。为了使这些操作更高效,许多计算机体系结构要求特定类型的数据在特定的地址上对齐。具体的对齐要求取决于数据类型和硬件架构。
常见的对齐要求包括字节对齐和字对齐。字节对齐要求数据的起始地址必须是数据类型的大小的整数倍。例如,一个整型变量(4字节)通常需要在地址是4的倍数的位置上对齐。字对齐要求数据的起始地址必须是数据类型大小的整数倍,并且整个数据的大小也必须是数据类型大小的整数倍。
存对齐的好处包括:

  1. 提高访问速度:当数据按照对齐要求存储时,CPU 可以更高效地读取和写入数据。对于未对齐的数据,可能需要多次访问内存才能获取完整的数据,而对齐的数据可以一次读取或写入。
  2. 减少内存碎片:内存对齐可以减少内存碎片的产生。如果数据没有按照对齐要求存储,可能会导致内存中存在未使用的空间,从而浪费内存资源。
    内存对齐通常由编译器自动处理,但在某些情况下,可能需要手动控制内存对齐。例如,在进行数据结构的序列化和反序列化操作时,需要确保数据按照特定的对齐规则进行存储和读取。
    总之,内存对齐是为了满足硬件对数据存储的要求,以提高内存读取和写入的效率,并减少内存碎片

C 里面有 malloc 了,为什么 C++ 还要引入 new

C++引入了new运算符是为了更好地支持对象的动态内存分配和构造。虽然C语言中有malloc函数可以进行内存分配,但它只是简单地分配一块内存空间并不会调用对象的构造函数
C++的new运算符除了分配内存外,**还会调用对象的构造函数来初始化对象。**这是因为C++是面向对象的语言,强调对象的生命周期管理和构造/析构过程。通过new运算符,我们可以在进行内存分配的同时,自动调用对象的构造函数进行初始化。
C++的new运算符还提供了更多的功能,如对数组的动态内存分配和构造,异常处理机制等。它可以根据对象的类型进行动态内存分配,并返回相应类型的指针,而不需要显式指定分配的字节数。
C++的new运算符还可以与delete运算符搭配使用,用于释放对象占用的内存空间,并在适当的时候调用析构函数进行资源的释放。而C语言中的mallocfree则是对应的内存分配和释放函数,不会自动调用对象的构造和析构函数
C++引入new运算符是为了提供更方便、安全和面向对象的内存分配和初始化机制。

有没有听过内存池,讲一下内存池

内存池是一种用于管理和分配内存的技术,旨在提高内存分配和释放的效率,减少动态内存分配的开销。
应用程序需要频繁地进行内存的分配和释放,而动态内存分配操作(如mallocnew)在频繁使用时可能会产生较大的开销。内存池通过预先分配一块较大的内存空间,并将其划分为多个固定大小的内存块(或称为内存块池)。应用程序可以从内存池中获取这些固定大小的内存块,而无需频繁地进行动态内存分配操作。
内存池可以分为静态内存池和动态内存池两种类型:

  1. 静态内存池:静态内存池在程序初始化阶段就分配了一块固定大小的内存空间,并将其划分为多个固定大小的内存块。这种内存池的大小在运行时不会发生变化
  2. 动态内存池:动态内存池可以在运行时根据需要动态地增加或减少内存空间。它可以根据内存需求的变化动态调整内存池的大小。
    内存池的优点是:
  • 提高内存分配和释放的效率:通过减少动态内存分配操作,内存池可以避免内存碎片的产生,提高内存分配和释放的效率。
  • 减少动态内存分配的次数:内存池通过预先分配内存块,避免了频繁的动态内存分配操作,从而减少了系统调用和内存管理的开销
  • 简化内存管理:应用程序可以从内存池中获取固定大小的内存块,而无需自己管理内存的分配和释放,简化了内存管理的复杂性。
    注意事项:
  • 内存池可能会占用更多的内存空间:由于内存池预先分配了一块较大的内存空间,可能会导致一部分内存浪费,因此需要合理估计内存池的大小。
  • 需要注意线程安全性:如果多个线程同时访问内存池,需要采取适当的同步措施来保证线程安全性。

内存池是一种常用的内存管理技术,适用于需要频繁进行内存分配和释放的场景,如游戏引擎、网络服务器等。

  1. 提高性能:内存池的预分配和固定大小的内存块可以减少内存分配和释放的开销,从而提高性能。频繁的动态内存分配操作可能会导致内存碎片和内存管理的开销,而内存池可以避免这些问题。
  2. 减少内存泄漏的风险:内存池的使用可以降低内存泄漏的风险。当使用动态内存分配操作时,如果忘记释放分配的内存,就会造成内存泄漏。而内存池在分配内存块后,可以通过内部管理机制追踪内存的使用情况,并在内存块不再使用时进行释放,从而减少内存泄漏的风险。
  3. 控制内存分配的行为:通过自定义内存池的实现,可以根据应用程序的需求来控制内存分配的行为。例如,可以实现特定的内存分配策略,如固定大小的内存块分配、内存对齐等。这样可以更好地满足应用程序的内存需求和性能要求。
    需要注意的是,内存池并非适用于所有场景。在一些情况下,如需要动态增长的数据结构、大量不同大小对象的分配等,使用内存池可能会产生较大的内存浪费和复杂性。因此,在使用内存池时,需要仔细评估场景和需求,并进行合理的设计和测试。

展开说一下 STL 中的内存池

在STL中,内存池被实现为一个名为std::allocator的模板类。std::allocator提供了内存分配和释放的接口,并可以与其他STL容器(如std::vectorstd::list等)配合使用。
std::allocator内存池的主要目的是优化STL容器的性能。STL容器在进行元素的插入、删除等操作时,需要频繁地进行内存分配和释放。为了避免频繁的动态内存分配操作带来的性能开销,allocator使用了内存池的技术。
具体来说,std::allocator维护了一个内存池**,该内存池中预先分配了一定数量的内存块**,每个内存块的大小为容器元素的大小。当容器需要进行内存分配时,std::allocator会首先从内存池中查找是否有可用的内存块。如果有可用的内存块,则直接分配其中一个内存块;如果没有可用的内存块,则会向系统申请新的内存
使用std::allocator的好处是可以避免频繁的动态内存分配和释放,从而提高容器操作的性能。此外,std::allocator还可以自定义内存池的实现,以适应特定的需求。
需要注意的是,std::allocator内存池是对STL容器的默认分配器,但并不是所有的STL容器都使用内存池。一些容器(如std::vector)使用了std::allocator内存池进行内存管理,而其他容器(如std::map)可能使用不同的内存管理策略。这取决于具体的实现和编译器。
总结起来,STL中的内存池通过std::allocator提供了一种优化STL容器性能的机制。它通过预先分配一些内存块,并在容器操作时避免频繁的动态内存分配和释放,从而提高了容器操作的效率。

池化的技术除了内存池外,你还听过哪些

  1. 线程池(Thread Pool):线程池是一种管理和重用线程的技术。它预先创建一组线程,这些线程可以在需要执行任务时被重复使用,避免了频繁地创建和销毁线程的开销。线程池通常用于并发和异步编程,可以提高程序的响应性能和资源利用率。
  2. 连接池(Connection Pool):连接池是用于管理和重用数据库连接的技术。在数据库访问过程中,频繁地建立和断开数据库连接可能会产生较大的开销。连接池通过预先建立一组数据库连接,并将这些连接保存在池中,可以避免频繁地创建和关闭连接,提高数据库访问的性能和效率。
  3. 对象池(Object Pool):对象池是用于管理和重用对象的技术。它预先创建一组对象,并将这些对象保存在池中。在需要使用对象时,可以从对象池中获取对象,使用完毕后将对象归还到池中,避免了频繁地创建和销毁对象的开销。对象池常用于资源密集型的场景,如线程池中的任务对象、连接池中的数据库连接等。
  4. 图片池(Image Pool):图片池是用于管理和缓存图片资源的技术。在图形应用程序中,频繁地加载和释放图片资源可能会导致较大的性能开销。图片池通过预先加载并缓存图片资源,并在需要时从池中获取图片,可以避免频繁的磁盘读取和内存分配操作,提高图形应用程序的性能和响应速度。
    这些池化技术的共同目标是通过预先分配和重用资源,避免频繁的资源分配和释放操作,从而提高系统的性能和效率。它们在不同领域和场景中发挥着重要的作用,并被广泛应用于各种软件系统和框架中。

什么是内存泄漏

内存泄漏(Memory Leak)是指在程序运行中,由于错误的内存管理导致已经分配的内存无法被正确释放,从而导致系统中的可用内存逐渐减少,最终耗尽所有可用内存资源。
内存泄漏通常发生在动态内存分配的场景下,如使用mallocnew等操作分配内存空间后,未能正确释放该内存空间。内存泄漏可能是因为以下几种情况:

  1. 内存分配后未释放:当通过动态内存分配函数(如mallocnew)分配了一块内存空间后,如果没有调用相应的内存释放函数(如freedelete)来释放该内存空间,就会导致内存泄漏。
  2. 引用计数错误:在一些使用引用计数的内存管理机制中,对象的引用计数可能存在错误的操作,导致对象无法正常释放。例如,引用计数未正确增加或减少,导致对象无法在不再被引用时被释放。
  3. 循环引用:当存在多个对象之间相互引用,并且这些对象之间的引用形成了循环时,可能会导致对象无法被正常释放。循环引用会导致引用计数无法达到零,从而使对象无法被自动释放。

如果服务器上出现了内存泄漏,应该怎么排查

  1. 监测内存使用情况:使用系统工具或第三方监测工具来监测服务器的内存使用情况。这些工具可以提供内存使用量、内存分配情况、内存泄漏等相关指标,帮助定位问题。
  2. 分析日志和错误报告:查看服务器的日志文件和错误报告,尤其关注与内存相关的错误信息。内存泄漏可能会导致错误或异常,所以对于频繁出现的错误或异常,需要仔细分析其背后的原因。
  3. 使用内存分析工具:使用专业的内存分析工具来检测和分析内存泄漏问题。这些工具可以帮助检测未释放的内存、找出引起泄漏的代码位置等。常见的内存分析工具包括Valgrind、AddressSanitizer、Heaptrack等。
  4. 代码审查:仔细检查代码,尤其是与内存分配和释放相关的部分。查看是否存在内存分配后未释放的情况,以及是否存在资源管理的错误。
  5. 增加日志和调试信息:在代码中增加额外的日志和调试信息,以便更好地跟踪内存使用和释放的情况。这有助于定位哪些资源没有被正确释放,或者哪些代码路径导致内存泄漏。
  6. 进行内存泄漏测试:编写专门的内存泄漏测试用例,模拟服务器运行过程中的不同场景,并监测内存使用情况。通过这些测试可以发现内存泄漏的存在,并帮助确定问题所在。
  7. 使用可视化工具:一些可视化工具可以将内存泄漏问题以图形化的方式展示,帮助开发者更直观地理解内存使用情况和泄漏点。例如,内存堆栈跟踪工具可以可视化显示内存分配和释放的调用关系。
  8. 逐步调试:使用调试器对程序进行逐步调试,观察内存分配和释放的过程。通过断点调试,可以确定是否存在未正确释放内存的代码路径。

内存泄漏可能会带来什么风险

  1. 内存资源耗尽:每次发生内存泄漏,都会导致一部分内存资源无法被回收,随着时间的推移,系统中的可用内存逐渐减少,最终可能导致系统的内存资源耗尽。
  2. 程序性能下降:内存泄漏会导致程序的内存使用效率下降,频繁的动态内存分配操作和未释放的内存会增加系统的内存压力,影响程序的性能和响应速度。
  3. 崩溃和不稳定:当内存泄漏严重时,系统的可用内存资源将被耗尽,导致程序崩溃或异常终止。此外,内存泄漏还可能导致内存碎片问题,进一步影响系统的稳定性。

malloc和new有什么区别

  1. 语法:malloc是C语言中的库函数,需要包含头文件<stdlib.h>,使用方式为void* malloc(size_t size),返回一个void*指针。而new是C++的运算符,使用方式为new Type,返回一个指向Type类型的指针。
  2. 类型安全:new是类型安全的,它在内存分配的同时会进行类型检查,并在分配失败时抛出std::bad_alloc异常。而malloc只是简单地分配一块内存空间,不会进行类型检查。
    3.** 内存大小计算**:new会根据指定的类型自动计算所需的内存大小,而malloc需要手动指定所需的字节数。
  3. 构造函数和析构函数的调用:new会自动调用对象的构造函数来初始化分配的内存,而malloc不会调用构造函数,只是分配一块原始的内存空间。同样,delete运算符会自动调用对象的析构函数来释放内存,而free函数只会释放内存,不会调用析构函数。

free和delete的区别

free不提供类型检查,delete提供更好的内存安全性
free只释放内存,不析构对象,delete是先析构对象,再释放内存

内存泄漏怎么办?

内存泄漏是指在程序运行过程中,分配的内存空间没有被释放,导致这些内存无法再被程序访问和利用,从而造成内存的浪费。内存泄漏可能会导致程序的性能下降、资源耗尽以及不可预测的行为。下面是一些处理内存泄漏的常见方法:

  1. 善用自动内存管理:C++11及以后的标准提供了智能指针(smart pointer)的机制,如std::unique_ptrstd::shared_ptr,它们可以自动管理动态分配的内存,当指针不再需要时会自动释放内存。使用智能指针可以避免手动释放内存的繁琐,并减少内存泄漏的风险。
  2. 显式释放内存:在使用new运算符分配内存后,需要在不再需要该内存时使用delete运算符显式释放内存。确保每次动态分配内存后,都有相应的delete语句用于释放内存。
  3. 定位内存泄漏点:使用内存泄漏检测工具来帮助定位内存泄漏的点。常见的内存泄漏检测工具包括Valgrind、AddressSanitizer和LeakSanitizer等。这些工具可以在运行时检测程序的内存使用情况,并指出可能的内存泄漏位置,帮助你及时发现和修复内存泄漏问题。
    4.** 建立良好的资源管理习惯**:确保在分配内存后,对应的释放代码应该在任何情况下都能够执行到。例如,遵循资源获取即初始化(Resource Acquisition Is Initialization,RAII)的原则,将资源的分配和释放逻辑放在同一个代码块或类的构造函数和析构函数中,确保资源能够在适当的时候得到释放。
  4. 行代码审查和测试:定期进行代码审查和测试,特别是关注内存管理的部分,检查是否存在潜在的内存泄漏问题。使用测试用例覆盖不同的代码路径,包括正常情况和边界情况,以确保程序在各种情况下都能正确地分配和释放内存。
  5. 使用专业的工具和分析器:使用内存分析工具和静态代码分析器来帮助检测和修复内存泄漏问题。这些工具可以帮助你分析代码,找出潜在的内存泄漏和资源管理问题,并提供解决方案和建议。

如何避免内存碎片

  • 外部碎片:指的是已分配内存块之间的未使用空间,通常由内存释放操作造成的。外部碎片会导致难以分配连续内存块。
  • 内部碎片:指的是已分配的内存块中的未使用部分,通常由于内存分配器为满足对齐要求而分配了更多内存,或者由于数据结构的尺寸大于实际存储的数据而产生。

以下是一些方法来避免内存碎片:

  1. 使用内存池:内存池是一种技术,它预先分配一块连续的内存,然后将这个内存划分为固定大小的块。这可以减少外部碎片,特别是在需要频繁分配和释放内存块的情况下。
  2. 合并和紧凑:定期合并和紧凑内存分配。这可以通过释放不再使用的内存块或合并多个相邻的内存块来实现。
  3. 使用内存池分配器:某些编程语言和库提供专门的内存分配器,允许你更好地控制内存分配和释放,以减少碎片。
  4. 避免频繁的小内存分配:尽量避免频繁分配小内存块,而是使用更大的内存块。这可以减少内部碎片。
  5. 使用固定大小的数据结构:确保数据结构的大小是固定的,避免动态改变数据结构的大小,从而减少内部碎片。
  6. 选择适当的内存分配策略:根据应用程序的需求,选择适当的内存分配策略,如首次适应(First Fit)或最佳适应(Best Fit),以最小化碎片。
  7. 内存回收:及时释放不再使用的内存,以避免外部碎片的累积。
  8. 使用内存映射文件:在某些情况下,使用内存映射文件可以减少外部碎片问题,因为文件映射通常是页面对齐的。

段错误是什么

指针没有初始化,指向非法内存

野指针和悬空指针

野指针:指向无效内存,指针释放后没有重置为nullptr
悬空指针:未初始化

内存泄漏和内存溢出

内存泄漏:忘记释放内存,使用智能指针
内存溢出:程序访问未分配的内存区域,栈溢出:递归深度过深,堆溢出:访问未分配,缓冲区溢出:写入大于目标缓冲区的大小,覆盖相邻内存区域

C++的内存结构

主要由栈区、堆区、静态/全局存储区和常量存储区组成
栈区:先进后出的数据结构,一段连续区域,每个函数都会创建一个称为栈帧的区域,用于存储函数参数、返回地址、函数局部变量,内存分配是自动的,函数调用结束自动销毁
堆区:动态分配内存的区域,大小并不固定,一个new对应一个delete,或者一个malloc对应一个free来管理
在程序任何地方都可以进行访问,但是需要手动管理内存
静态/全局存储区:存储全局变量和静态变量
全局变量在程序开始时创建,程序结束时释放
静态变量的作用域可以是全局的(函数外部定义)或局部的(函数内部定义,存储在栈帧中),
常量存储区:存放一些常量

具体实现可能因编译器、操作系统和运行环境而异。C++还支持其他一些特殊的存储区域
自由存储区:动态分配内存的另一个区域

堆区和栈区的区别

分配方式

堆:手动分配和释放,new-delete,malloc-free
栈: 自动分配的,进入一个函数,分配一块内存空间,函数结束自动释放

内存分布:

栈:连续区域
堆:不一定连续

内存大小

栈:一般是1M
堆:一般是4GB

分配速度

栈:相对较快,以固定大小的栈帧为单位进行
堆:相对较慢,需要在堆中查找足够大的连续内存空间来满足分配请求

什么情况下使用堆区什么情况使用栈区

使用堆区:

1)对象的生命周期需要在函数调用结束后继续存在
2)需要动态分配大量的内存空间,而栈空间有限
3)需要共享数据
4)需要动态的创建对象和销毁

使用栈区:

1)对象的生命周期需要在函数调用结束自动销毁
2)需要分配的内存空间较小且固定大小
3)对象不需要函数之间共享或在函数调用之外继续存在
4)需要较快的内存分配和释放速度

注意

堆区的内存分配和释放需要手动管理,需要程序员负责分配和释放内存,避免内存泄漏和内存溢出等问题。
而栈区的内存管理由编译器自动处理,无需手动干预。
在实际编程中,通常会根据具体的需求和情况来选择使用堆区或栈区。需要综合考虑内存大小、对象的生命周期、内存分配速度和内存管理复杂性等因素。

栈溢出

  1. 递归深度过大:递归函数在每次递归调用时都会将一些信息(如局部变量和返回地址)压入栈中。如果递归深度过大,栈可能会耗尽内存,导致栈溢出。
  2. 大型局部变量:在函数中声明大型的局部变量数组时,这些数组可能占用大量栈空间,导致栈溢出。
  3. 无限循环:无限循环或递归调用没有终止条件会导致栈不断增长,最终溢出。
  4. 过多的函数参数:函数参数通常也存储在栈中。如果函数具有大量参数,栈的空间可能会被消耗完。
  5. 递归函数缺少终止条件:递归函数必须包含终止条件,否则它将无限递归下去,最终导致栈溢出。
  6. 爆炸式增长的数据结构:某些数据结构,如树或图的深度优先遍历,可能导致栈增长得非常快,如果没有适当的终止条件,可能导致栈溢出。
  7. 缓冲区溢出:在函数中操作缓冲区时,如果没有正确检查数据长度,可能会导致缓冲区溢出,从而覆盖栈上的数据,导致栈溢出。
  8. 递归的错误实现:错误的递归实现,例如在递归调用之前不释放资源或不正确地处理数据,可能导致栈溢出

栈的内存满了是怎么重新分配的?

栈内存的分配是由操作系统和编程语言运行时环境控制的。当栈的内存空间满了,系统通常会引发栈溢出错误。栈溢出是一种严重的运行时错误,通常会导致程序终止,因为栈的内存空间是有限的,而栈的大小在程序启动时就已经分配好了。

在栈内存满了的情况下,通常不会自动重新分配栈的内存,因为栈的大小在程序运行时是固定的。重新分配栈内存可能涉及复杂的内存管理和程序状态转移,因此通常不是自动执行的。

一般来说,为了避免栈溢出,开发者应该小心管理递归深度、局部变量和函数参数的大小,确保递归函数具有终止条件,避免无限循环,以及正确处理缓冲区和数据结构,以减小栈的内存消耗。

如果程序需要更大的内存空间来存储大量数据,通常会使用堆内存(通过动态内存分配函数如mallocnew等)来分配和释放内存,而不是依赖栈。堆内存的分配和释放通常需要显式的编程操作,而不会自动发生,因此开发者需要负责管理堆内存。这允许程序在运行时动态地分配和释放内存,而不受栈大小的限制。

递归怎么控制深度

1)递归终止条件
2)使用计数器变量跟踪递归的深度

STL里面的内存管理

std::allocatorstd::allocator 是STL中的默认分配器,用于动态内存分配和释放。它提供了 allocatedeallocate 函数来分配和释放内存块,并支持对象的构造和销毁。许多STL容器都使用 std::allocator 作为默认分配器。

一级分配,2级分配

一级分配和二级分配是内存管理中的两种常见分配方式,用于将内存分配给应用程序或数据结构。它们的原理如下:

  1. 一级分配(First Fit Allocation)

    • 一级分配是最简单的分配方式,通常由操作系统的内存管理器执行。
    • 在一级分配中,操作系统维护一个内存空闲块列表,该列表包含可用的内存块。当应用程序请求分配内存时,操作系统会查找列表中第一个足够大的内存块,然后将该内存块分配给应用程序。
    • 一级分配通常用于传统的单处理器系统,它具有较低的内存管理复杂度,但可能导致内存碎片问题。
  2. 二级分配(Two-Level Allocation)

    • 二级分配是一种改进的内存分配方式,通常用于多处理器系统或需要更高内存管理效率的情况。
    • 在二级分配中,操作系统维护两个内存空闲块列表,一个用于小内存块,一个用于大内存块。
    • 当应用程序请求分配内存时,操作系统首先查找小内存块列表。如果找到足够大的内存块,它将分配给应用程序。否则,它会查找大内存块列表,分配一个大内存块并将其划分为小块,然后将其中一个小块分配给应用程序。
    • 二级分配可以减少内存碎片,提高内存管理效率。

需要注意的是,实际的内存分配算法可能更复杂,具体取决于操作系统和应用程序的需求。此外,现代操作系统通常还会实施更高级的内存管理策略,如分页和虚拟内存,以提供更灵活和高效的内存管理。内存分配是一个庞大的主题,涉及到多种算法和技术,以满足各种应用和性能需求。

面向对象

多继承有没有听过菱形继承,讲它可能会出现的问题

菱形继承是指一个派生类同时继承自两个直接或间接基类,而这两个基类又共同继承自一个共同的基类。这样形成的继承关系图形状类似于菱形
A
/
B C
\ /
D
在这个例子中,类 D 继承自类 B 和类 C,而类 B 和类 C 都继承自类 A。这样,类 D 就有了两个路径可以访问类 A 中的成员。
菱形继承可能导致以下问题:
冗余和二义性:当派生类 D 尝试访问从基类 A 继承的成员时,由于存在两条路径,会导致成员变量和成员函数的冗余,增加了代码的复杂性。此外,如果基类 A 中的成员在类 B 和类 C 中有不同的实现,那么在派生类 D 中访问这些成员时会产生二义性,编译器无法确定具体使用哪个实现。

菱形继承出现的问题该怎么解决

虚基类解决冗余和二义性:为了解决菱形继承带来的问题,C++ 提供了虚基类(Virtual Base Class)的概念。通过将基类 A 声明为虚基类,派生类 D 就只会有一个从 A 继承的成员副本,避免了冗余和二义性。使用虚基类时,需要使用虚基类语法来声明和初始化虚基类。
菱形继承是多继承中的一个特殊情况,需要谨慎使用,因为它可能引发代码冗余和二义性的问题。在设计多继承的继承关系时,应考虑使用虚基类来解决菱形继承带来的问题,或者重新设计继承结构以避免菱形继承的出现。

C++三大特性

  1. 封装(Encapsulation):
    封装是一种将数据和对数据的操作封装在一起的机制。通过封装,我们可以将相关的数据和函数组合成一个对象,对象对外部隐藏其内部的实现细节,只暴露有限的接口供其他对象进行访问。这种将数据和操作封装在对象内部的方式,提高了代码的模块化、可维护性和安全性。
  2. 继承(Inheritance):
    继承是一种通过构建类之间的关系,实现代码的重用和扩展的机制。通过继承,一个类(子类或派生类)可以继承另一个类(父类或基类)的属性和方法,并可以在此基础上添加新的成员或修改继承的成员。继承支持类之间的层次结构,允许在子类中重用和修改父类的代码,提高了代码的复用性和可扩展性。
  3. 多态(Polymorphism):
    多态是一种允许使用基类类型来处理派生类对象的特性。通过多态,我们可以根据对象的实际类型,在运行时选择合适的方法进行调用。多态能够提供更大的灵活性和扩展性,使得我们可以编写更通用、可复用的代码,同时减少了对具体类型的依赖。

多态怎么实现的

在C++中,多态通过虚函数(virtual function)和继承机制来实现。以下是多态的实现步骤:

  1. 基类定义虚函数:在基类中,通过在函数声明前加上 virtual 关键字来定义虚函数。虚函数是一种特殊的成员函数,它可以在派生类中被重写(覆盖)
class Base {
public:
    virtual void foo() {
        // 基类中的虚函数实现
    }
};

2.派生类重写虚函数:在派生类中,可以通过在函数声明前加上 override 关键字来重写基类的虚函数。重写虚函数时,函数签名必须和基类的虚函数完全一致

class Derived : public Base {
public:
    void foo() override {
        // 派生类中的虚函数实现
    }
};
  1. 通过基类指针或引用调用虚函数:可以通过基类指针或引用来调用虚函数。在运行时,会根据对象的实际类型来动态选择合适的虚函数实现
Base* ptr = new Derived();  // 使用基类指针指向派生类对象
ptr->foo();  // 调用虚函数,根据对象的实际类型选择合适的实现

4.当通过基类指针或引用调用虚函数时,实际执行的是派生类中重写的虚函数,即根据对象的实际类型进行动态绑定,实现了多态性。
为了实现多态,必须使用基类指针或引用来操作对象,以允许在运行时根据对象的实际类型来选择虚函数的实现。
如果基类的析构函数是虚函数(通过在析构函数前加上 virtual 关键字),那么在删除派生类对象时,会先调用派生类的析构函数来确保正确释放资源。

虚表说一下

虚函数表(Virtual Function Table,简称 VTable)是C++中实现多态的一种机制。
1)当类中存在虚函数时,编译器会为该类生成一个虚函数表,其中存储了该类中所有虚函数的地址。
2)在运行时,当通过指针或引用调用虚函数时,程序会根据对象的实际类型查找到对应的虚函数表,然后通过表中存储的函数地址来调用正确的函数实现。
3)每个含有虚函数的类都有自己的虚函数表,虚函数表通常存储在对象的内存布局中的第一个位置。每个虚函数表中包含了该类所有虚函数的函数指针,每个虚函数在虚函数表中的位置是固定的,因此可以通过偏移量来访问。
4)对于继承关系中的子类,其虚函数表会包含父类中的虚函数,但子类所新增加的虚函数仍会在新的虚函数表中存储。同时,如果子类覆盖了父类的虚函数,那么子类的虚函数表中对应位置的函数指针将指向子类的实现。

说一下虚函数表和虚指针

1)每个包含虚函数的类都有一个虚函数表,虚函数表是一个数组,存储类中虚函数的地址
2) 对于每个类的实例,编译器在对象的内存布局中插入一个指向该类的虚函数表的指针,称为虚指针
3)对象调用虚函数的时候,实际通过虚指针找到虚函数表,根据函数在表中的索引来调用正确的虚函数,允许在运行期间确定要调用哪个实际函数
4)派生类没有虚函数会从基类继承虚函数表并覆盖

虚函数是怎么实现的?

  1. 虚函数是通过虚函数表(Virtual Function Table)和虚函数指针(Virtual Function Pointer)实现的。
  2. 当一个类中声明了虚函数时,编译器会为该类创建一个虚函数表。虚函数表是一个特殊的数据结构,它存储了该类所有虚函数的地址。每个对象实例也会包含一个指向虚函数表的虚函数指针,称为虚表指针(vptr)
  3. 当通过一个对象调用虚函数时,编译器会使用对象的虚表指针来查找虚函数表,并根据函数在虚函数表中的位置找到相应的虚函数地址。然后,通过该地址来调用对应的虚函数。这个过程被称为动态绑定(Dynamic Binding)或后期绑定(Late Binding),因为实际调用的虚函数是在运行时决定的。
  4. 当存在继承关系时,派生类会继承基类的虚函数,并且可以重新定义(覆盖)这些虚函数。如果派生类重新定义了某个虚函数,它会在自己的虚函数表中存储相应的虚函数地址。这样,通过派生类对象调用该虚函数时,会使用派生类的虚表指针和虚函数表来确定实际调用的函数。
  5. 虚函数机制只适用于通过指针或引用调用的情况,而不适用于直接使用对象名调用虚函数。当通过指针或引用调用虚函数时,编译器会使用动态绑定机制来确定实际调用的函数,而通过对象名调用虚函数时,编译器会使用静态绑定(Static Binding)机制,直接调用基类中定义的函数。
  6. 虚函数的实现使得多态性(Polymorphism)成为可能,允许通过基类指针或引用调用派生类中的函数,实现了面向对象编程的一个重要特性。

析构函数为什么有时候要是虚函数?

当一个基类指针或引用指向一个派生类对象,并且通过该基类指针或引用删除该对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中的资源无法正确释放,造成资源泄漏或不一致的状态。
通过将基类的析构函数声明为虚函数,可以解决这个问题。当基类的析构函数是虚函数时,通过基类指针或引用删除对象时,会根据指针或引用实际指向的对象类型调用相应的析构函数。这样,派生类的析构函数也会被正确调用,从而确保派生类中的资源得到正确释放。

class Base {
public:
    virtual ~Base() {
        // 基类的析构函数声明为虚函数
        // ...
    }
};

class Derived : public Base {
public:
    ~Derived() {
        // 派生类的析构函数
        // ...
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 使用基类指针删除对象
    return 0;
}

在上述代码中,当使用基类指针ptr删除对象时,如果基类的析构函数不是虚函数,只会调用基类的析构函数,导致派生类的析构函数不会被调用,从而可能引发问题。但是,如果基类的析构函数被声明为虚函数(virtual ~Base()),那么使用基类指针删除对象时,将会调用派生类的析构函数,确保派生类中的资源得到正确释放。
因此,为了正确实现多态性的销毁,当一个类可能被继承时,建议将其析构函数声明为虚函数。这样可以确保通过基类指针或引用删除对象时,能够调用到派生类的析构函数,从而正确释放资源。

一个类空指针可以调用虚函数吗?可以调用普通函数吗?

不可以,可以

  1. 一个类的空指针可以调用虚函数,但是调用虚函数时会导致未定义行为(undefined behavior)。这是因为虚函数的调用是通过虚函数表(vtable)来实现的,而空指针并没有指向任何对象的虚函数表,因此无法正确地调用虚函数。在实践中,这通常会导致程序崩溃或产生其他异常行为。
  2. 对于普通的非虚函数,空指针可以调用。在这种情况下,由于非虚函数的调用是通过静态绑定(static binding)实现的,编译器会根据指针的类型确定调用哪个函数。因为空指针没有指向任何对象,所以调用非虚函数时会导致空指针解引用错误(dereference error)或运行时异常。
  3. 为了避免空指针调用虚函数或非虚函数时的问题,应当在调用前先进行指针的空指针判断,确保指针有效再进行调用。例如,可以使用条件语句或断言来检查指针是否为空。

怎么理解C++的封装 继承 多态

  1. 封装(Encapsulation)就像一个保险箱
    封装就像是将相关的数据和功能放在一个保险箱里面,只对外界提供必要的接口来访问和操作这些数据和功能。其他人无法直接访问保险箱内部的细节,只能通过接口进行操作。这样做的好处是保护数据的安全性和代码的可维护性。
  2. 继承(Inheritance)就像是父子传承
    继承就像是从父亲那里继承财产和特征一样,一个类可以继承另一个类的属性和方法。子类(派生类)继承了父类(基类)的特性,可以直接使用父类中的方法和数据,同时还可以添加自己的独特特性。这样可以减少代码的重复性,提高代码的复用性和可扩展性
  3. 多态(Polymorphism)就像是变身术
    多态就像是一个人可以变身成不同的形态,具备不同的能力和行为。在面向对象编程中,多态允许以统一的方式处理不同类型的对象,根据对象的实际类型来调用相应的方法。这样可以实现灵活的代码设计和扩展,提高代码的可读性和可维护性。
    举例:
    假设我们有一个动物园的场景,有不同种类的动物,比如狗、猫和鸟。我们可以把每种动物都看作是一个类,它们都有共同的特征(如名字和年龄)和行为(如发出声音)。这就是封装将数据和方法封装在各自的类中。
    定义一个基类 Animal,其中包含共同的特征和行为。然后派生出不同的子类,比如 Dog、Cat 和 Bird,它们继承了** Animal 的特性并可以添加自己的特性**。这就是继承,子类继承了父类的属性和方法。
    可以通过多态的方式处理这些动物对象。例如,可以定义一个函数,接收 Animal 类型的参数,并调用其发声的方法。当我们传入不同的子类对象时,函数会根据对象的实际类型来调用相应的发声方法。这就是多态,同一种操作可以根据不同的对象类型表现出不同的行为

编译相关

静态链接和动态链接的区别

源代码编译流程.png
库:函数的集合 作用:共享代码
静态、动态指链接
程序编译过程中,在链接阶段,程序生成的汇编文件进行链接,生成可执行文件

image.png

1.静态库(.a或者.lib)

静态链接库在程序编译时被连接到目标代码中参与编译;链接时将库完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝;生成可执行程序之后,静态链接库不需要(因已将函数拷贝到可执行文件中)
优点:

  1. 静态库对函数库的链接是放在编译时期完成的
  2. 程序在运行时与函数库再无瓜葛,方便程序移植,放在任何环境当中都可以执行;

缺点:

  1. 浪费空间和资源,所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件(体积较大),包含相同的公共代码;
  2. 每次库文件升级的话,都要重新编译源文件,很不方便。如果静态库进行更新则应用该库的所有程序都需要重新编译(全量更新)

2.动态库(.so或者.dll)

程序运行时由系统动态加载动态库到内存,供程序调用,系统只加载一次,多个程序共用,节省内存
优点:

  1. 更加节省内存并减少页面交换;
  2. DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性
  3. 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
  4. 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。

缺点:

  1. 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。
  2. 使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
  3. 速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统死掉。

区别:

1.时期
静态库在编译时链接,在链接时拷贝
动态库在运行时链接
2.资源
静态库在每次使用时将全部链接进可执行程序,浪费资源。
动态库在使用时访问动态库中函数,节省资源。
3.更新升级
静态库更新,则每个使用该静态库的程序都需要更新,不易于更新升级
动态库仅更新自身,易于更新升级
4.包含其他库
静态链接库不能再包含其他动态链接库
动态链接库可以包含其他动态链接库

C++程序编译的过程:预编译-编译-汇编-链接-可执行程序

预编译:.h,.cpp,.c文件->.i文件
** 展开宏定义,删除注释**
编译:.i文件->.s文件
** 对预编译后的代码进行语法和语义分析,生成汇编代码**
汇编:.s文件->.o文件
** 将汇编代码转成机器可执行的目标文件,包含了机器指令,符号表等**
链接:.o文件->可执行程序.exe,.out
** 将目标文件与所需的库链接,生成最终的可执行文件,解析符号引用,不同目标文件符号连接**
** 最终生成的可执行文件可以直接在操作系统上运行**

如果函数没有定义在哪个阶段能知道? 编译

如果符号缺失呢? 链接

静态库和动态库

静态库:编译时链接,代码合并到可执行文件,优点:加载速度快,可移植性高 缺点:可执行文件大
#include是静态库
动态库:运行时链接,优点:代码重用和共享库,节约空间 缺点:移植性差,性能开销大

#include< >和#include“ ”的区别:

一、引用的头文件不同
#include< >引用的是编译器的类库路径里面的头文件。
#include“ ”引用的是你程序目录的相对路径中的头文件。
二、用法不同
#include< >用来包含标准头文件(例如stdio.h或stdlib.h).
#include“ ”用来包含非标准头文件。
三、调用文件的顺序不同
#include< >编译程序会先到标准函数库中调用文件。
#include“ ”编译程序会先从当前目录中调用文件。
四、预处理程序的指示不同
#include< >指示预处理程序到预定义的缺省路径下寻找文件。
#include“ ”指示预处理程序先到当前目录下寻找文件,再到预定义的缺省路径下寻找文件。

编译过程,为什么要有中间代码

过程:词法分析,语法分析,语义分析,生成中间代码
作用:跨平台编译,可维护性,多语言支持,模块化

动态链接库和静态链接库的区别

1. 静态链接库:

  • 静态链接库是在编译时被链接到应用程序中的库文件。链接时,库的代码和数据被复制到最终的可执行文件中,因此生成的可执行文件独立包含了所有库函数的代码。
  • 静态链接库的优点是在部署时不需要依赖外部的库文件,因为所有的代码都被包含在了可执行文件中。这样可以简化部署过程,确保程序在不同环境下的运行一致性。
  • 缺点是每个可执行文件都包含了库的完整副本,导致可执行文件的大小较大。而且如果同一个库被多个应用程序使用,每个应用程序都需要包含一份完整的副本,造成资源浪费。
  1. 动态链接库:
    • 动态链接库是在运行时被加载和链接到应用程序中的库文件。应用程序在需要使用库函数时,通过动态链接的方式将库文件加载到内存,并在内存中执行相应的代码。
    • 动态链接库的优点是可以实现代码共享,多个应用程序可以共享同一个库的实例。这样可以减小可执行文件的大小,节省内存占用,并且方便进行库的升级和维护。
    • 缺点是在部署时需要确保所依赖的库文件存在于系统中,否则应用程序无法正常运行。此外,由于动态链接库在运行时被加载,会引入一定的性能开销。
      总结:
  • 静态链接库在编译时将库的代码和数据复制到可执行文件中,部署时不需要外部依赖,但可执行文件较大。
  • 动态链接库在运行时加载库文件,可以实现代码共享和方便的升级,但需要确保库文件存在,并会有一定的性能开销。

动态链接库在什么地方被调用

  1. 应用程序调用:应用程序可以通过编程语言提供的接口(如函数调用)直接调用动态链接库中的函数或方法。这是最常见的场景,应用程序通过动态链接库提供的功能来实现特定的业务逻辑。
  2. 插件/扩展模块:动态链接库可以作为插件或扩展模块的形式存在,应用程序在需要时动态加载和使用这些库。这种方式允许应用程序在运行时动态地添加、移除或切换功能模块,以满足不同的需求。
  3. 操作系统调用:操作系统本身也可能使用动态链接库来提供一些系统级的功能或服务。应用程序可以通过操作系统提供的接口调用这些动态链接库来实现对底层系统的访问和控制。
  4. 第三方库调用:动态链接库通常用于封装和提供可复用的功能库。其他开发者或开发团队可以使用这些库来实现自己的应用程序。开发者可以通过引入动态链接库,并使用其提供的函数或方法来快速构建自己的应用程序。

#include和动态链接库的区别

#include 是C/C++编程语言中的预处理指令,用于在源代码中包含头文件(header file)。它的作用是将头文件中的声明内容插入到源代码中,以便在编译时使用头文件中定义的函数、结构、宏等。
与动态链接库相比,#include 具有以下区别:

  1. 功能和作用:#include 是用于将头文件的声明插入到源代码中,以便在编译时使用其中的定义。它在编译阶段将头文件的内容直接合并到源代码中,使得编译器可以访问头文件中的定义。
    而动态链接库是一个独立的二进制文件,包含了已编译的代码和数据,可以在运行时被加载和链接到应用程序中。动态链接库提供了可执行代码的共享,可以供多个应用程序使用,实现了代码的复用和模块化。
  2. 链接方式:#include 的链接是静态的,在编译时将头文件的内容直接合并到源代码中。而动态链接库是在运行时进行动态加载和链接的,应用程序在需要时通过动态链接的方式将库文件加载到内存中。
  3. 部署和依赖:使用#include 的程序在编译时需要确保所依赖的头文件存在,并且将源代码、头文件和编译后的目标文件打包在一起进行部署。而动态链接库在部署时需要确保所依赖的库文件存在于系统中,应用程序只需链接到相应的库文件即可。
  4. 可执行文件大小:由于#include将头文件的内容直接插入到源代码中,编译后生成的可执行文件会包含所有使用到的头文件的代码和数据,因此可执行文件的大小可能会增加。而动态链接库可以减小可执行文件的大小,因为多个应用程序可以共享同一个库的实例。
    总结:#include是用于将头文件的声明插入到源代码中,在编译时进行静态链接。而动态链接库是在运行时进行动态加载和链接的独立二进制文件,提供了代码的共享和模块化

介绍一下C++异常处理是什么

异常处理是一种编程技术,用于在程序执行过程中检测和处理潜在的错误或异常情况。异常是指在程序运行过程中出现的意外或异常的情况,例如除以零、访问不存在的内存地址、文件读取错误等。
C++ 异常处理机制的核心概念如下:

  1. 抛出异常(Throwing Exceptions):当在程序中遇到错误或异常情况时,可以使用 throw 关键字抛出异常。异常对象可以是任意类型,通常是标准库或自定义的异常类的实例。
  2. 捕获异常(Catching Exceptions):使用 try-catch 块来捕获异常。在 try 块中放置可能抛出异常的代码,并在 catch 块中捕获和处理异常。catch 块中的代码将根据捕获的异常类型执行相应的操作。
  3. 异常处理程序(Exception Handlers):catch 块中的代码被称为异常处理程序。它会根据捕获的异常类型执行相应的逻辑,例如输出错误消息、进行日志记录、进行资源清理等。
  4. 异常传播(Exception Propagation):如果一个函数内部抛出了异常,但没有在该函数内部处理,异常将传播到调用该函数的地方。如果调用方也没有处理异常,那么异常将继续传播,直到遇到处理异常的地方或程序终止。
    C++ 的异常处理机制通过以下关键字和语法实现:
  • try:用于包裹可能抛出异常的代码块。在 try 块中,如果发生异常,控制流将转到匹配的 catch 块。
  • catch:用于捕获和处理异常。catch 后面可以跟一个或多个异常类型,用于指定要捕获的异常类型。当异常类型匹配时,相应的 catch 块将执行。
  • throw:用于抛出异常。throw 后面可以跟一个异常对象,将该异常对象抛出。
  • std::exception:是 C++ 标准库中定义的基类,用于表示异常的基本类。可以自定义继承自 std::exception 的异常类来表示特定的异常情况。
    使用 C++ 异常处理机制,可以更好地处理和管理程序中的错误情况,增加程序的健壮性和可靠性。开发人员可以根据具体的业务需求定义和抛出自定义的异常,并通过捕获和处理异常来进行适当的错误处理和恢复操作。

C++异常有哪些

在 C++ 中,有几种类型的异常可以使用。以下是 C++ 异常的常见类型:

  1. 标准异常(Standard Exceptions):C++ 标准库中定义了一组标准异常类,它们是从 std::exception 类派生而来的。这些异常类用于表示常见的异常情况,可以用于捕获和处理不同类型的异常。一些常见的标准异常类包括:
    • std::exception:表示通用异常的基类。
    • std::logic_error:表示逻辑错误的异常类,例如无效参数、无效操作等。
    • std::runtime_error:表示运行时错误的异常类,例如文件打开失败、内存分配失败等。
  2. 自定义异常(Custom Exceptions):开发人员可以根据需要定义自己的异常类,以表示特定的异常情况。自定义异常类可以继承自标准异常类或其他自定义异常类,以提供更具体和详细的异常信息。
  3. 标准库异常(Standard Library Exceptions):除了标准异常类外,C++ 标准库中的一些组件也定义了自己的异常类,用于表示特定的异常情况。例如,STL 容器类(如 std::vectorstd::map 等)在某些操作失败时可能会引发异常,例如迭代器越界、内存分配失败等。
    在 C++ 中,异常的类型可以是任何类的对象。异常对象通常通过 throw 语句抛出,并在 try-catch 块中进行捕获和处理。通过捕获不同类型的异常,可以根据需要执行相应的异常处理操作。

C++编译时异常和运行时异常有什么区别

编译时异常(Compile-time Exceptions)是在编译代码期间检测到的异常。这些异常通常由编译器发现,指示了代码中的语法错误、类型错误或逻辑错误等问题。编译时异常会导致代码无法通过编译,因此在运行程序之前必须解决这些异常。例如,如果你在C++中使用了未声明的变量,编译器会报告一个编译时异常。
运行时异常(Runtime Exceptions)是在程序运行时发生的异常。这些异常通常是由于逻辑错误、算法错误、内存访问错误或运行时条件导致的问题。与编译时异常不同,运行时异常在代码编译和链接阶段不会被检测到。运行时异常可以通过异常处理机制来捕获和处理。如果未正确处理运行时异常,程序可能会终止或导致不可预测的行为。例如,在C++中,如果你访问一个空指针或者在数组中越界,就会引发运行时异常
总结起来,编译时异常是在编译阶段检测到的错误,导致代码无法通过编译。而运行时异常是在程序运行时发生的错误,需要使用异常处理机制来捕获和处理。

关键字

什么情况下用内联,什么时候用宏定义

使用内联函数的情况:

  1. 函数体简短且频繁调用:内联函数适合用于函数体简短的情况,这样可以减少函数调用的开销。如果函数体很长,内联函数的代码会被频繁复制,增加了代码的体积,反而会降低性能。
  2. 需要类型检查和语法分析:内联函数可以进行参数类型检查和返回值类型检查,避免了宏定义的类型错误问题。内联函数还可以使用局部变量和其他函数特性,增强了代码的可读性和可维护性。
  3. 类成员函数:在类定义中声明的成员函数默认是内联函数,因为它们通常都是短小的函数,并且频繁调用。
    使用宏定义的情况:
  4. 简单的代码替换需求:宏定义可以实现简单的文本替换,适用于一些简单的代码替换需求,如常量定义、简单的计算等。
  5. 特定的宏需求:宏定义可以用于实现一些特定的功能,如条件编译、调试信息的输出等。宏定义可以在预处理阶段进行文本替换,灵活性较高。
  6. 特殊需求或特定平台:有些特殊需求或特定的平台可能需要使用宏定义。例如,一些底层硬件相关的操作可能需要使用宏定义,因为它们需要直接操作硬件寄存器或内存。
    需要注意的是,宏定义的使用可能会引发一些问题,如宏名冲突、不易调试等。因此,在使用宏定义时,需要谨慎考虑,并遵循良好的编码规范。在一些情况下,内联函数可能是更好的选择,因为它提供了类型检查和函数特性的优势。

用过static吗?如果在头文件定义,可以吗?

定义全局变量时使用static,意味着该变量的作用域只限于定义它的源文件中,其它源文件不能访问。既然这种定义方式出现在头文件中,那么可以很自然地推测:包含了该头文件的所有源文件中都定义了这些变量,即该头文件被包含了多少次,这些变量就定义了多少次
在头文件中定义static变量会造成变量多次定义,造成内存空间的浪费,而且也不是真正的全局变量。应该避免使用这种定义方式。

static关键字

可以控制变量和函数的生命周期、作用域和可访问性
静态变量:只能初始化一次,函数调用结束后保持其值,直到程序结束,存放在静态存储区
静态函数:直接通过类名调用,不需要创建类的实例,只能访问静态成员(静态变量和静态函数)
静态成员变量:所有实例共享, 而不是每个实例都有自己的副本 ,可以通过类名或对象来访问
文件作用域:只在当前源文件可见,不能被其他源文件访问
静态函数是类不是对象

如果将一个成员声明为 static,它与普通成员变量有什么区别

当将一个成员声明为 static 时,它与普通成员变量有以下区别:

  1. 存储位置:普通成员变量在每个类的实例中都有一份独立的存储空间,而静态成员变量(static 成员变量)在整个类的所有实例之间共享一份存储空间。静态成员变量存储在静态数据区,不属于对象的一部分。
    2.** 初始化和生命周期**:静态成员变量在程序运行期间只会被初始化一次,而普通成员变量则在每个对象被创建时都会被初始化。静态成员变量的初始化通常在定义时或在类的实现文件中进行。静态成员变量的生命周期延续到整个程序运行期间
  2. 访问方式:普通成员变量通过对象实例访问,而静态成员变量可以通过类名直接访问,也可以通过对象实例访问。在类的内部,可以直接使用静态成员变量的名称访问它。
  3. 可见性和作用域:静态成员变量对于整个类都是可见的,可以被类的所有对象共享。而普通成员变量的可见性仅限于对象的作用域内。
  4. 内存占用:静态成员变量在内存中只有一份拷贝,无论类的实例有多少个,因此可以节省内存空间。而普通成员变量在每个对象中都会占用内存空间。
    需要注意的是,静态成员变量必须在类的声明外进行定义和初始化,以确定其存储空间。可以通过类名和作用域解析运算符 :: 来访问静态成员变量。例如,对于类 ClassName 的静态成员变量 staticMember,可以使用 ClassName::staticMember 进行访问。
    静态成员变量常用于存储属于整个类而不是特定对象的共享数据,或用于计数、记录状态等全局信息

静态成员函数什么时候创建,属于类还是对象

  1. 静态成员函数在编译阶段就已经创建,它们属于类而不是类的对象。静态成员函数与类的实例化对象无关,可以直接通过类名来调用,而不需要创建对象实例。无论创建了多少个类的对象,静态成员函数都只有一个实例
  2. 静态成员函数常用于处理与类相关的操作,例如访问静态成员变量、执行与类相关的计算等。由于它们不依赖于对象的状态,静态成员函数通常不访问非静态成员变量,而是处理与整个类或类的静态数据相关的任务。
  3. 要调用静态成员函数,可以使用类名和作用域解析运算符"::"来访问,例如:ClassName::staticMemberFunction()

宏定义和内联函数的区别

用于代码优化的不同机制

  1. 展开时机
    • 宏定义:宏定义是在预处理阶段(编译前)进行文本替换的。宏是一种简单的文本替换机制,没有类型检查,也没有作用域。
    • 内联函数:内联函数是在编译阶段(编译时)进行的,编译器会尝试将内联函数的代码插入到调用它的地方。内联函数通常有类型检查和作用域。
  2. 类型安全
    • 宏定义:宏不进行类型检查,只是简单地进行文本替换。这可能导致潜在的类型错误或副作用。
    • 内联函数:内联函数会进行类型检查,因此更安全。编译器会确保参数的类型正确,并处理类型转换。
  3. 调试和可读性
    • 宏定义:由于宏是在预处理阶段进行替换,因此在编译时的调试信息通常不包含宏的信息。这可能使调试更加困难。此外,宏定义可能导致代码可读性较差,因为它们通常较复杂。
    • 内联函数:内联函数会在编译时展开,因此调试信息通常包含内联函数的信息,使调试更容易。此外,内联函数通常更具可读性,因为它们具有函数的结构和类型信息。
  4. 代码大小和性能
    • 宏定义:宏的展开可能导致生成较大的代码,因为它在每个调用点都会复制文本。这可以提高性能,但会增加可执行文件的大小。
    • 内联函数:内联函数通常生成较小的代码,因为它们的代码在每个调用点都插入,减少了函数调用的开销。这有助于提高性能并减少可执行文件的大小。

constexpr是什么意思

constexpr 是C++中的关键字,用于指示在编译时计算表达式的值,以提供编译时常量化和性能优化。它的主要用途包括:

  1. 编译时常量化constexpr 用于声明可以在编译时求值的常量表达式。这允许编译器在编译时计算表达式的值,而不是在运行时。这有助于提高性能,减少运行时开销,因为不需要在运行时重复计算表达式的值。
  2. 在数组大小和模板参数中使用constexpr 使您能够在编译时计算数组的大小和在模板参数中使用常量表达式,从而增加了编译时的灵活性和性能。
  3. 函数的编译时评估constexpr 函数是指那些在编译时计算其结果的函数。它们可以用作常量表达式,这意味着它们可以在编译时执行,从而提供了更高的性能和更广泛的用途。

explicit关键字

编译器将不再允许这种隐式类型转换,要求使用显式的构造函数调用,防止可能引起混淆或错误的情况

int main(){ // 隐式类型转换 MyClass obj = 42; 
  // 编译器会调用 MyClass(int x) 构造函数
  //显式类型转换 MyClass objExplicit = MyClass(42); 
  // 显式调用构造函数return0;
}

const有什么用

1)const声明常量,变量值不可改变
2)修饰函数参数,不可以修改传入参数
3)修饰成员函数,不可以修改对象的成员变量
4)修饰成员变量,表示变量在对象创建后不可被修改
5) 修饰指针const int*p: 常量指针,是一个指针,指向的数据不可改变,指针的指向可以改变
_int const *_p:指针常量,是一个常量,指向的数据可改变,指针的指向不可以改变

volatile关键字作用

保持内存的可见性,编译期间不要优化,每次使用变量之前要重新读取,而不使用之前缓存的值

CPP的封装继承多态是什么

面向对象OOP的三大特性,提高代码的可维护性,可重用性和安全行
封装:将数据与操作数据的方法打包成类,私有化数据成员,提供公共接口与数据进行交互,
**继承:**派生类继承基类,扩展它们或者覆盖它们,有助于代码重用
多态:基类指针指向派生类对象,实现动态绑定,静态多态(重载),运行时多态(动态多态

多态是怎么实现的

通过虚函数和继承实现,把需要实现多态的函数在基类声明为虚函数,子类中进行重写覆盖
虚函数表在子类和父类都有,子类覆盖或新增自己的虚函数,程序运行时会使用虚函数表来确定要调用的虚函数

说一下虚函数表和虚指针

1)每个包含虚函数的类都有一个虚函数表,虚函数表是一个数组,存储类中虚函数的地址
2) 对于每个类的实例,编译器在对象的内存布局中插入一个指向该类的虚函数表的指针,称为虚指针
3)对象调用虚函数的时候,实际通过虚指针找到虚函数表,根据函数在表中的索引来调用正确的虚函数,允许在运行期间确定要调用哪个实际函数
4)派生类没有虚函数会从基类继承虚函数表并覆盖

const和宏定义的区别?有什么优点?

const和宏定义(#define)是用于定义常量的两种不同方式。
const是一种关键字用于声明具有常量值的变量。它指定一个变量的值在初始化后不能被修改const关键字可以应用于各种数据类型,包括基本类型(如整数、浮点数等)和自定义类型(如结构体、类等)。例如:

const int MAX_VALUE = 100;
const float PI = 3.14;

宏定义是一种预处理指令,用于在编译之前将标识符替换为指定的文本。它使用#define关键字定义,并且通常用于定义常量或简单的代码片段。宏定义不会进行类型检查,仅仅是简单的文本替换。例如:

#define MAX_VALUE 100
#define PI 3.14
  1. 类型安全性: const提供类型安全性,因为它可以应用于各种数据类型,并且在编译时进行类型检查。宏定义则没有类型安全性,它只是进行简单的文本替换,不进行类型检查。
  2. 作用域: const具有块级作用域,它可以被限定在特定的作用域内。宏定义是全局的,它在整个程序中都可见。
  3. 调试和可读性: const在调试过程中提供更好的可读性,因为它们可以被调试器解析和显示。宏定义在调试过程中可能会引起困惑,因为它们只是文本替换,不会在调试器中显示真实的标识符。
  4. 代码长度和复杂性: 宏定义可以用于替换大量的代码,从而减少代码的长度和复杂性。但是,过度使用宏定义可能会导致代码难以维护和理解。const通常用于声明单个常量,因此更容易理解和维护。
    综上所述,const提供了更好的类型安全性、作用域控制、可读性和调试能力,而宏定义提供了更大的灵活性和代码简化的可能性。根据具体的使用场景和需求,选择合适的方式来定义常量。

C++语言特性

指针与引用的区别

  1. 指针是⼀个变量,存储的是⼀个地址,引⽤跟原来的变量实质上是同⼀个东⻄,是原变量的别名
  2. 指针可以有多级,引⽤只有⼀级
  3. 指针可以为空,引⽤不能为NULL且在定义时必须初始化
  4. 指针在初始化后可以改变指向,⽽引⽤在初始化之后不可再改变
  5. sizeof指针得到的是本指针的⼤⼩, sizeof引⽤得到的是引⽤所指向变量的⼤⼩
  6. 当把指针作为参数进⾏传递时,也是将实参的⼀个拷⻉传递给形参,两者指向的地址相同,但不是同⼀个变量,在函数中改变这个变量的指向不影响实参,⽽引⽤却可以
  7. 引⽤本质是⼀个指针,同样会占4字节内存;指针是具体变量,需要占⽤存储空间(具体情况还要具体分析)
  8. 引⽤在声明时必须初始化为另⼀变量,⼀旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量⽽不初始化,等⽤到时再指向具体变量
  9. 引⽤⼀旦初始化之后就不可以再改变(变量可以被引⽤为多次,但引⽤只能作为⼀个变量引⽤);指针变量可以重新指向别的变量。
  10. 不存在指向空值的引⽤,必须有具体实体;但是存在指向空值的指针

封装

数据和代码捆绑在⼀起,避免外界⼲扰和不确定性访问。
封装,也就是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信
的进⾏信息隐藏,例如:将公共的数据或⽅法使⽤public修饰,⽽不希望被访问的数据或⽅法采⽤private修饰。

继承

访问权限

public的变量和函数在类的内部外部都可以访问。
protected的变量和函数只能在类的内部和其派⽣类中访问。
private修饰的元素只能在类内访问
image.png

继承权限

image.png

多态

指相同对象收到不同消息或不同对象收到相同消息时产⽣不同的实现动作

  1. 编译时多态性:通过重载函数实现(相同对象收到不同消息)
  2. 运⾏时多态性:通过虚函数实现(不同对象收到相同消息)

实现多态有⼆种⽅式:覆盖(override),重载(overload)
覆盖(重写):是指⼦类重新定义⽗类的虚函数的做法
重载:是指允许存在多个同名函数,⽽这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者
都不同)。例如:基类是⼀个抽象对象——⼈,那教师、运动员也是⼈,⽽使⽤这个抽象对象既可以表示教师、也可以表示运动员。

如果一个类有很多子类,那么这个类中的方法是被子类共享的还是一人一个副本

在面向对象编程中,如果一个类有多个子类,类中的方法通常是被子类共享的,而不是每个子类都有一个副本。
当一个类定义了一个方法,该方法可以在该类的所有实例中使用。当子类继承父类时,子类会自动获得父类的所有方法和属性。子类可以使用继承的方法,并且还可以重写这些方法以适应子类的特定需求
当子类重写一个方法时,子类的实例将使用子类中的方法实现,而不是父类中的方法。这称为方法重写。通过方法重写,子类可以覆盖父类中的方法,以实现不同的行为。
因此,如果一个类有很多子类,并且没有进行方法重写,那么这个类中的方法是被子类共享的,子类可以直接使用父类的方法。如果某个子类重写了父类的方法,那么该子类的实例将使用子类中的方法实现,而其他子类实例仍然使用父类的方法实现。

举个例子说一下

在这个示例中,Rectangle 和 Circle 类都继承了 Shape 类,并重写了父类的 calculate_area 函数以实现各自特定的面积计算逻辑。尽管它们是不同的子类,但它们共享了父类的方法,并根据自己的实现进行了方法重写

#include <iostream>
class Shape {
public:
    virtual double calculate_area() {
        return 0;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double calculate_area() override {
        return width * height;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double calculate_area() override {
        return 3.14 * radius * radius;
    }
};

int main() {
    Rectangle rectangle(4, 5);
    double rectangle_area = rectangle.calculate_area();
    std::cout << "矩形的面积:" << rectangle_area << std::endl;
    
    Circle circle(3);
    double circle_area = circle.calculate_area();
    std::cout << "圆的面积:" << circle_area << std::endl;
    
    return 0;
}

说一下你对 this 指针的理解

this是一个指向当前对象的指针,它可以在类的成员函数内部使用。this指针指向调用该成员函数的对象本身,允许在函数内部访问对象的成员变量和成员函数。
当我们在类的成员函数中引用成员变量时,编译器会隐式地使用 this指针来访问对象的成员。例如,如果一个类有成员变量 value,在成员函数中使用 value 将被解释为 this->value。
this 指针的使用主要有以下几个方面:
1.** 解决命名冲突**:当成员变量与函数参数或局部变量名称相同时,可以使用 this 指针来显式指明访问的是成员变量而不是局部变量。

class MyClass {
private:
    int value;
public:
    void setValue(int value) {
        this->value = value;
    }
};
  1. 在类的成员函数中返回当前对象的引用:在链式调用或者某些特定的设计模式中,可以使用 this 指针返回当前对象的引用,以便实现连续的操作。
class MyClass {
private:
    int value;
public:
    MyClass& increment() {
        ++value;
        return *this;
    }
};
  1. 在类的成员函数中传递当前对象的指针:当需要将当前对象传递给其他函数时,可以使用 this 指针传递当前对象的地址。
class MyClass {
public:
    void process() {
        someFunction(this);
    }
};

需要注意的是,对于静态成员函数和非成员函数,它们没有 this 指针。静态成员函数是属于整个类而不是对象的,因此没有对特定对象的隐式访问。
总结起来,this 指针提供了对当前对象的隐式访问,允许在类的成员函数中访问对象的成员和操作对象本身。

说一下移动构造函数和拷贝构造函数的区别

移动构造函数(Move Constructor)和拷贝构造函数(Copy Constructor)是C++中用于对象初始化的特殊成员函数,它们在对象创建和复制时起到不同的作用。它们的区别如下:

  1. 语法:

    • 拷贝构造函数的语法:ClassName(const ClassName& other)
    • 移动构造函数的语法:ClassName(ClassName&& other) noexcept
  2. 作用对象:

    • 拷贝构造函数用于创建一个新对象,该对象是已存在对象的精确副本
    • 移动构造函数用于创建一个新对象,该对象从已存在对象中“移动”资源,而不是进行复制
  3. 资源转移:

    • 拷贝构造函数复制已存在对象的所有成员和资源,并创建一个新对象。这通常涉及资源的深拷贝,即在堆上分配新的内存,并将数据复制到新的内存中
    • 移动构造函数将已存在对象的资源转移(移动)给新对象,避免了复制的开销。这通常涉及资源的浅拷贝,即将资源的指针或句柄从一个对象转移到另一个对象,而不涉及实际数据的复制
  4. 性能:

    • 拷贝构造函数需要执行深拷贝,包括内存的分配和数据的复制,可能会有较大的开销。
    • 移动构造函数避免了数据的复制和额外的内存分配,通常比拷贝构造函数更高效
  5. 右值引用:

    • 拷贝构造函数接受const左值引用作为参数,因为它需要创建副本并保持原对象不变。
    • 移动构造函数接受右值引用作为参数,因为它将会“窃取”原对象的资源。
  6. 在C++11及以后的标准中,通过使用右值引用和移动语义,可以明确指定移动构造函数。移动构造函数的出现主要是为了提高性能,在处理临时对象、函数返回值、容器元素插入等场景中特别有用。通过使用移动构造函数,可以避免不必要的内存分配和复制,提高程序的效率。

  7. 移动构造函数通常应该标记为noexcept,以指明它在执行时不会抛出异常。这样可以使得一些算法和容器在移动对象时进行优化,提高程序的稳定性和性能。

  8. 移动构造函数和拷贝构造函数在对象初始化和资源转移方面具有明显的区别。移动构造函数通过资源的移动而不是复制,提高了程序的性能。处理大型对象或需要频繁进行对象复制的情况下,使用移动构造函数可以显著提高性能。移动构造函数将资源的所有权从一个对象转移到另一个对象,而不进行昂贵的复制操作。这对于临时对象、函数返回值和容器元素等场景特别有用。

  9. 使用移动构造函数还可以避免不必要的内存分配和释放。当一个对象即将被销毁时,其资源可以被移动到另一个对象,从而避免了复制操作和额外的内存开销。这对于动态分配的资源(如堆内存)尤为重要,可以减少内存分配器的负担,提高内存管理的效率。

  10. 移动构造函数并不总是比拷贝构造函数更好。对于小型对象或不涉及资源转移的情况,拷贝构造函数可能更加高效。在某些情况下,编译器会自动优化对象的复制,将拷贝操作转换为移动操作,以提高效率。因此,选择移动构造函数还是拷贝构造函数要根据具体情况进行权衡和选择。

  11. 在C++11及以后的标准中,为了支持移动语义,还引入了移动赋值运算符(Move Assignment Operator)和移动语义相关的函数(如std::move和std::forward)。这些特性一起提供了更强大的资源管理和性能优化能力,使得移动操作更加方便和高效。

移动构造函数通过资源的移动而不是复制,提供了一种高效的对象初始化和资源转移方式。它在处理大型对象、避免不必要的内存分配和提高程序性能方面具有重要作用。然而,对于小型对象或不涉及资源转移的情况,拷贝构造函数仍然是常用的选择。

解释一下右值引用是什么,它与左值引用的区别

右值引用(Rvalue reference)是C++11引入的一种新类型的引用,用于标识右值(临时值)并允许对其进行特殊操作。右值引用与左值引用的主要区别在于它们可以绑定到不同类型的表达式,并且对绑定的对象的操作具有不同的语义。

  1. 左值引用(Lvalue reference):
    • 左值引用使用&符号声明,如int&
    • 左值引用只能绑定到左值表达式,即具有标识符且可寻址的表达式。
    • 左值引用表示对一个具名对象的引用,可以进行读取和写入操作,因为它们指向具有持久性的对象
  2. 右值引用(Rvalue reference):
    • 右值引用使用&&符号声明,如int&&
    • 右值引用可以绑定到右值表达式,即临时对象或无法寻址的表达式。
    • 右值引用表示对一个临时对象或即将被销毁的对象的引用,通常用于资源的转移和移动语义。
      右值引用主要用于以下两个方面:
  • 移动语义(Move Semantics):右值引用使得移动构造函数和移动赋值运算符的定义成为可能。通过移动资源而不是进行深拷贝,可以在对象之间高效地转移资源的所有权,避免不必要的内存分配和复制,提高程序性能。
  • 完美转发(Perfect Forwarding):右值引用结合std::forward可以实现参数的完美转发,即在函数中将传递给函数的参数以相同的方式转发给另一个函数。这对于泛型编程和函数包装器(如std::bind)非常有用,可以保留传递参数的值类别(左值还是右值)。
    使用右值引用的代码片段:
void processValue(int& value) {
    std::cout << "Lvalue reference: " << value << std::endl;
}

void processValue(int&& value) {
    std::cout << "Rvalue reference: " << value << std::endl;
}

int main() {
    int x = 5;

    processValue(x);           // 调用左值引用版本
    processValue(10);          // 调用右值引用版本
    processValue(std::move(x)); // 调用右值引用版本

    return 0;
}

根据传递给processValue函数的参数类型,将分别调用左值引用版本和右值引用版本。左值引用版本接受具名对象的引用,右值引用版本接受临时对象或即将被销毁的对象的引用。
3. 右值引用的特点:

  • 右值引用是一种新的引用类型,使用&&符号进行声明。
  • 右值引用可以绑定到临时对象(右值)或即将被销毁的对象,无法绑定到左值表达式。
  • 右值引用具有可修改性,可以用于进行移动操作和完美转发。
  • 通过std::move函数可以将左值转换为右值引用,表示对其进行移动操作。
  1. 右值引用的用途:
    • 移动语义:通过移动构造函数和移动赋值运算符,利用右值引用可以实现资源的高效转移,避免不必要的复制操作。移动语义对于管理动态分配的资源(如内存)特别有用,可以避免额外的内存分配和释放
    • 完美转发:通过右值引用和std::forward函数,可以实现参数的完美转发。这对于泛型编程和函数包装器非常重要,可以保留传递参数的值类别(左值还是右值),确保参数以相同的方式传递给其他函数。

右值引用的引入使得C++语言更加灵活和高效。它提供了一种新的引用类型,通过与移动语义和完美转发结合使用,可以实现更高效的资源管理和更灵活的参数传递。通过适当地使用右值引用,可以避免不必要的复制、提高程序的性能,并提供更强大的语言特性来支持现代C++编程。

纯虚函数是什么

在C++中,纯虚函数(Pure Virtual Function)是一种特殊的虚函数,它在基类中声明但没有提供具体的实现。纯虚函数通过在函数声明后面添加= 0来标识
纯虚函数的主要作用是定义一个接口,强制派生类提供对应的实现。基类中的纯虚函数只是一个接口的规范,没有具体的实现,因此无法直接实例化基类对象。
基类中的至少一个纯虚函数会使得该基类成为抽象类(Abstract Class),抽象类不能被实例化。只有派生类实现了基类中的所有纯虚函数,才能实例化派生类对象
纯虚函数的语法如下所示:

class Base {
public:
    virtual void pureVirtualFunction() = 0;
    // ...
};

需要注意的是,基类中的纯虚函数可以有其它成员函数和数据成员,而且基类也可以同时包含普通的虚函数。
派生类在继承了基类的纯虚函数后,必须实现这些函数,否则派生类也会成为抽象类。派生类可以选择在自己的定义中使用override关键字来明确表明它正在重写基类中的纯虚函数。

#include <iostream>
class Base {
public:
    virtual void pureVirtualFunction() = 0;
};

class Derived : public Base {
public:
    void pureVirtualFunction() override {
        std::cout << "Derived::pureVirtualFunction()" << std::endl;
    }
};

int main() {
    // Base base;  // 错误,无法实例化抽象类
    Derived derived;
    derived.pureVirtualFunction();  // 输出:Derived::pureVirtualFunction()

    Base* basePtr = &derived;
    basePtr->pureVirtualFunction(); // 输出:Derived::pureVirtualFunction()

    return 0;
}

总结:纯虚函数是一种在基类中声明但没有实现的虚函数,它用于定义接口并要求派生类提供对应的实现。纯虚函数使得基类成为抽象类,不能被实例化。派生类必须实现基类中的纯虚函数,才能实例化派生类对象。

sizeof指针和sizeof引用有什么区别

sizeof是一个运算符,用于计算类型或表达式的字节大小。

  1. sizeof指针:对于指针类型,sizeof返回的是指针本身的字节大小,而不是指针所指向对象的大小。在大多数平台上,指针的大小是固定的,通常为4字节或8字节(取决于指针的位数)。这是因为指针存储的是内存地址,而不是实际的对象。
  2. sizeof引用:对于引用类型,sizeof返回的是引用所指向对象的字节大小。引用在内存中并不占用额外的空间,它只是对象的一个别名。因此,sizeof引用得到的结果与所引用对象的大小相同。

sizeof运算符在编译时求值而不是运行时。它返回的是静态的字节大小信息,不会随着程序的运行而改变。

C++的多态是怎么实现的

C++的多态性是通过虚函数(Virtual Function)和基类指针/引用来实现的。多态性允许通过基类指针或引用调用派生类对象的成员函数,根据实际对象的类型动态地确定要调用的函数。
以下是多态的实现方式:

  1. 定义基类(Base Class):基类是一个抽象的概念,其中包含了要在派生类中共享的属性和行为。通常,基类中的至少一个成员函数需要被声明为虚函数。
class Base {
public:
    virtual void someFunction() {
        // 虚函数的默认实现
    }
    // ...
};
  1. 定义派生类(Derived Class):派生类继承了基类的成员函数和数据成员,并可以重写基类中的虚函数。
class Derived : public Base {
public:
    void someFunction() override {
        // 派生类中对虚函数的实现
    }
    // ...
};
  1. 使用基类指针或引用进行多态调用:可以使用基类的指针或引用来引用派生类对象,并通过虚函数来实现多态调用。
Base* ptr = new Derived();  // 创建派生类对象,并使用基类指针引用
ptr->someFunction();  // 调用虚函数,根据实际对象类型决定调用派生类的实现
delete ptr;  // 注意要手动释放内存

在上述代码中,通过基类指针ptr调用someFunction()虚函数时,实际上会根据ptr指向的派生类对象类型,动态地调用派生类的实现。
关键点是在基类中将要被派生类重写的成员函数声明为virtual,这样就能实现运行时的多态性。通过基类指针或引用来操作派生类对象,可以根据实际对象的类型来决定调用哪个版本的虚函数。这使得多态性成为C++中的重要特性,能够实现灵活的对象行为和可扩展的代码设计

虚函数具体介绍一下

虚函数是C++中用于实现多态性的一种机制。它允许通过基类指针或引用调用派生类对象的成员函数,并根据实际对象的类型动态地确定要调用的函数。

  1. 声明和定义:在基类中,通过将成员函数声明为virtual来标识它为虚函数。虚函数可以有默认实现,也可以声明为纯虚函数(没有实际实现的函数,纯虚函数用= 0表示),使基类成为抽象类。
class Base {
public:
    virtual void someFunction() {
        // 虚函数的默认实现
    }

    virtual void pureVirtualFunction() = 0;
    // ...
};
  1. 动态绑定:使用基类指针或引用来引用派生类对象时,根据实际对象类型动态绑定(Dynamic Binding)将在运行时确定要调用的函数。这样,通过基类指针或引用调用虚函数时,实际上调用的是派生类的实现。
Base* ptr = new Derived();  // 创建派生类对象,并使用基类指针引用
ptr->someFunction();  // 调用虚函数,根据实际对象类型决定调用派生类的实现
  1. 覆盖和重写:派生类可以重写(Override)基类中的虚函数,提供自己的实现。重写时需要使用override关键字,确保正确覆盖基类的虚函数。
class Derived : public Base {
public:
    void someFunction() override {
        // 派生类中对虚函数的实现
    }
    // ...
};
  1. 虚析构函数:如果基类中含有虚函数,通常也需要将析构函数声明为虚析构函数,以确保正确地释放派生类对象的资源。这样做可以保证在使用基类指针删除派生类对象时,会调用派生类的析构函数。
class Base {
public:
    virtual ~Base() {
        // 虚析构函数
    }
    // ...
};

虚函数的使用使得通过基类指针或引用操作派生类对象时,可以根据实际对象的类型来确定要调用的函数,实现多态性。这为面向对象的设计和代码组织提供了更大的灵活性和可扩展性。

纯虚函数能在基类中实现吗

在C++中,纯虚函数是没有实际实现的虚函数,通常用于定义基类的接口,要求派生类必须提供自己的实现。因此,纯虚函数通常在基类中没有具体的实现。
然而,在某些情况下,纯虚函数也可以在基类中实现。这样做的目的是为了提供一个默认的实现,但仍然要求派生类进行重写以满足自己的需求。在这种情况下,需要在基类的纯虚函数声明后面提供一个定义,使用= 0表示它是纯虚函数。

class Base {
public:
    virtual void pureVirtualFunction() = 0;  // 纯虚函数声明

    virtual void someFunction() {
        // 纯虚函数的默认实现
        // 可以在基类中提供一个默认实现
        // 但仍然要求派生类进行重写
        // ...
    }
    // ...
};

void Base::someFunction() {
    // 基类纯虚函数的默认实现
    // 可以在这里提供一个具体的实现
    // 但这个实现通常并不完整,只是为了提供一个默认行为
    // ...
}

pureVirtualFunction()是基类中的纯虚函数声明,没有提供实际的实现。而someFunction()是基类中的虚函数,它被定义为提供纯虚函数的默认实现。这样做可以为纯虚函数提供一个通用的默认行为,但仍然要求派生类进行重写以满足自己的需求。
在基类中提供纯虚函数的默认实现并不常见,一般情况下我们更倾向于将纯虚函数留空,由派生类提供具体的实现。这样能够更好地遵循面向对象设计的原则,确保派生类必须实现纯虚函数。

C++析构可不可以抛异常

在C++中,析构函数可以抛出异常,但这样做是不推荐的,并且可能会导致程序的不确定行为。
当析构函数抛出异常时,如果没有合适的异常处理机制,程序将会终止并调用 std::terminate 来终止程序的执行。这是因为在析构函数抛出异常后,对象的析构过程无法正常完成,资源可能没有正确释放,程序处于不一致的状态。
为了确保程序的稳定性和可预测性,通常建议在析构函数中不要抛出异常,或者在析构函数内部适当处理异常,以确保异常不会被传播到析构函数的调用方。
如果在析构函数中抛出异常,并且没有适当的异常处理机制,可能会导致以下问题:
1.** 资源泄漏**:如果析构函数抛出异常,可能导致对象持有的资源无法正确释放,从而产生资源泄漏。
2. 不确定状态:当析构函数抛出异常时,对象可能已经部分析构完成,但又无法完全析构,导致对象的状态处于不确定的状态。这可能会对程序的后续操作产生不可预测的影响。
3. 内存泄漏:如果在析构函数抛出异常后,对象的析构函数再次被调用(比如在异常处理中重新析构对象),可能会导致多次析构,从而产生内存泄漏或其他内存错误。
因此,为了保证程序的稳定性和可预测性,通常建议在析构函数中避免抛出异常,或者在析构函数内部适当处理异常,确保对象的析构过程能够正常完成

虚函数的作用?

  1. 实现运行时多态:通过使用虚函数,可以在运行时根据对象的实际类型来调用相应的函数。这允许在基类指针或引用上调用派生类的特定实现。通过多态性,可以在不知道对象的具体类型的情况下编写通用的代码,提高代码的灵活性和可扩展性。
  2. 支持基类指针或引用的多态性:基类指针或引用可以指向派生类的对象,并通过调用虚函数来实现对派生类的成员函数的访问。这种机制允许以一致的方式处理不同类型的对象,使得代码更加通用和可复用。
  3. 覆盖(override)和扩展:派生类可以通过覆盖基类中的虚函数来提供自己的实现。这使得派生类可以修改或扩展继承的行为。通过重写虚函数,可以实现对基类的方法进行定制化,满足具体的需求。
  4. 动态绑定:虚函数使用动态绑定的机制,即在运行时根据对象的实际类型决定要调用的函数。这与静态绑定(非虚函数)不同,静态绑定在编译时根据引用或指针的声明类型来决定要调用的函数。动态绑定使得程序能够根据实际运行时的情况来选择正确的函数实现。

纯虚函数是什么,为什么需要纯虚函数?

纯虚函数是在基类中声明但没有提供实现的虚函数。纯虚函数通过在函数声明的末尾使用**= 0**来表示。基类中包含纯虚函数的类称为抽象类,而不能实例化抽象类的对象。
纯虚函数的主要目的是为了定义一个接口,强制要求派生类提供相应的实现。纯虚函数相当于一个占位符,用于表示派生类必须重写该函数,以便使派生类变为可实例化的具体类。通过将函数声明为纯虚函数,基类可以定义一组接口规范,而具体的实现由派生类提供。
纯虚函数的使用有以下几个主要原因:

  1. 接口定义:纯虚函数可以用于定义抽象基类的接口。它们为派生类提供了一组必须实现的函数,确保派生类在具体化时符合基类的规范。
  2. 多态性:纯虚函数和动态绑定结合使用,允许通过基类指针或引用调用派生类的实现。这种多态性的机制使得可以根据对象的实际类型调用正确的函数实现,提供了更灵活和可扩展的编程模型。
  3. 约束和规范:纯虚函数可以强制派生类提供特定的实现,确保每个派生类都满足基类定义的规范。这有助于提高代码的一致性和可维护性,同时还提供了一种代码文档化的方式。
    需要注意的是,包含纯虚函数的类不能直接实例化只能用作派生类的基类派生类必须实现基类中的纯虚函数,否则派生类本身也会成为抽象类。

影响 C++ class 类的大小的因素有哪些?

  1. 成员变量的大小:类中的成员变量的类型和数量会直接影响类的大小。较大的数据类型或者多个成员变量会增加类的大小。
  2. 对齐方式(Alignment):为了提高内存访问效率,编译器可能会在类的成员变量之间添加额外的空间,以对齐数据。对齐方式由编译器和编译器选项决定。对齐方式可能会导致类的大小增加,尤其是当成员变量的大小不是对齐方式的整数倍时。
  3. 继承和虚函数:如果类继承自其他类或者存在虚函数,那么额外的内存空间将用于存储继承的基类或虚函数表指针。继承和虚函数的存在可能会增加类的大小。
  4. 静态成员变量和静态函数:静态成员变量和静态函数在类级别上存在,并与任何特定的对象实例无关。它们的内存空间通常在程序加载时就被分配,并且不计算在类的大小中。
  5. 虚继承和多重继承:当类使用虚继承或多重继承时,可能会引入额外的指针或数据来处理继承关系,从而增加类的大小。
  6. 内存对齐和填充:为了满足对齐要求,编译器可能会在类的成员变量之间插入额外的填充字节。这些填充字节不存储任何数据,仅用于对齐目的,但会增加类的大小。

编译器对类的布局和对齐的具体实现会因编译器、编译选项和目标平台等因素而有所不同。类的实际大小可能会受到编译器和环境的影响。可以使用sizeof运算符来获取类的大小.

新特性后 STL 有啥变化?比如vector 有啥新操作?

STL

1)std::array是封装了固定大小的容器,其结合了C风格数组的性能和C++可访问性容器的优点,如支持容器大小,可赋值,随机访问等。
2)std::forward_list单向链表,节省了内存,同时又有比list更好的运行时性能
forward_list只有一个直接访问元素的接口:front()
3)**std::unordered_map,**哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。适合需要高效率查询的情况
**4)std::unordered_set,**基于哈希表的无序集合,具有快速查找、删除和添加等优点,插入时不会自动排序

vector:新增emplace 关键字

emplace_back() 和 push_abck() 的区别是:push_back() 在向 vector 尾部添加一个元素时,会创建一个临时对象,然后再将这个临时对象移动或拷贝到 vector 中(如果是拷贝的话,事后会自动销毁先前创建的这个临时元素);而 emplace_back() 在实现时,则是直接在 vector 尾部创建这个元素,省去了移动或者拷贝元素的过程。

Cpp 的锁 mutex 是怎么实现的?读锁,写锁,读写锁是什么?

  1. C++中的锁mutex是一种保护共享资源的同步机制,它可以确保在同一时间只有一个线程可以访问共享资源。mutex的实现依赖于操作系统提供的原语,通常是使用互斥量(mutex)实现的。
  2. 读锁和写锁是一种特殊的锁,在多线程环境下用于保护共享数据的读写操作。读锁允许多个线程同时读取共享数据,但是不允许有线程写入数据。写锁只允许一个线程写入共享数据,并且不允许其他线程读取或写入数据。这种锁的实现称为"读写锁"(Read-Write Lock)。
  3. 读写锁是一种特殊的锁,它允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。读写锁可以提高对共享数据的并发访问效率,因为多个线程可以同时读取共享数据,只有在需要写入数据时才需要对数据进行互斥访问。
  4. std::mutex是一个互斥量,它提供了基本的互斥操作,如lock()和unlock()。std::shared_mutex是一个读写锁,它提供了shared_lock()和unique_lock()两个操作,其中shared_lock()用于获取读锁,unique_lock()用于获取写锁。

为什么会有拷贝构造函数,主要用在哪些场景

拷贝构造函数是一种特殊的构造函数,用于创建一个对象时,通过复制另一个同类对象的数据来初始化新对象。它的声明形式通常是类名(const 类名& other),其中"other"是要被复制的对象。
拷贝构造函数主要用在以下场景:

  1. 对象的复制:当需要创建一个新对象,并且希望它的数据与另一个对象相同时,可以使用拷贝构造函数来进行复制操作。这在函数参数传递和返回对象时特别有用。
  2. 动态内存分配:当类中包含指针成员变量,并在构造函数中为其分配内存时,拷贝构造函数用于复制指针所指向的内存内容,以确保新对象与原对象具有相同的数据。
  3. 派生类的拷贝:当派生类需要从基类中继承数据时,拷贝构造函数可用于复制基类的数据成员,以保证派生类对象的完整性。

需要注意的是,如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,该函数按位复制对象的数据成员。然而,对于包含指针或资源管理的类,这样的默认实现可能会导致浅拷贝问题,因此可能需要显式定义拷贝构造函数来执行深拷贝操作,确保正确的对象复制和资源管理。

值传递的方式

值传递:
形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。
指针传递: 指针传递参数本质上是值传递的方式,它所传递的是一个地址值
形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
引用传递:
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递有什么区别吗?
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)
指针传递和引用传递一般适用于
函数内部修改参数并且希望改动影响调用者。对比指针/引用传递可以将改变由形参“传给”实参(实际上就是直接在实参的内存上修改,不像值传递将实参的值拷贝到另外的内存地址中才修改)。
另外一种用法是:当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递给函数,这样在函数内部修改并且返回后,调用者可以拿到被修改过后的变量,也相当于一个隐式的返回值传递吧。

 #include<iostream>
 using namespace std;

//值传递
void change1(int n){
     cout<<"值传递--函数操作地址"<<&n<<endl; //显示的是拷贝的地址而不是源地址 
     n++;
}
 
//引用传递
void change2(int & n){
     cout<<"引用传递--函数操作地址"<<&n<<endl; 
     n++;
}

//指针传递
void change3(int *n){
     cout<<"指针传递--函数操作地址 "<<n<<endl; 
    *n=*n+1;
} 

int  main(){
    int n=10;
    cout<<"实参的地址"<<&n<<endl;
    change1(n);
    cout<<"after change1() n="<<n<<endl;
    change2(n);
    cout<<"after change2() n="<<n<<endl;
    change3(&n);
    cout<<"after change3() n="<<n<<endl;
    return true;
}

image.png

C++open文件,从上到下做了什么

  1. 包含相关的头文件:你需要包含 头文件来使用文件输入/输出操作
  2. 创建文件流对象:使用 fstream类型的对象来表示文件流。你可以选择 ofstream类型来表示输出流(用于写入文件)或者 ifstream 类型来表示输入流(用于读取文件),或者你也可以使用 fstream 类型来表示可同时进行读写的文件流
    3.** 打开文件**:使用文件流对象调用open方法,并提供文件名和打开模式作为参数。打开模式可以是输入模式、输出模式或者二者的组合
    如果你想以只读方式打开一个文件,你可以使用以下代码:
std::ifstream file;
file.open("filename.txt", std::ios::in);
  1. 检查文件是否成功打开:在打开文件后,你可以使用 is_open方法检查文件是否成功打开。如果文件成功打开,is_open方法将返回 true,否则返回 false
    你可以使用以下代码检查文件是否成功打开:
if (file.is_open()) {
   // 文件成功打开
} else {
   // 文件打开失败
}
  1. 进行读取或写入操作:根据打开文件的模式,你可以使用相应的方法来读取或写入文件内容。
    如果你使用输入流对象(如 ifstream或 fstream),可以使用 >> 运算符或 getline方法来从文件中读取数据。
    你可以使用以下代码从文件中读取一行数据:
std::string line;
std::getline(file, line);

如果你使用输出流对象(如 ofstream 或fstream),你可以使用 <<运算符或 write 方法来向文件中写入数据。

file << "Hello, world!";
  1. 关闭文件:在你完成文件操作后,应该使用 close 方法关闭文件流。
    例如,你可以使用以下代码关闭文件流:
file.close();

以上是使用C++中的"open"函数打开文件的一般流程。请注意,在进行文件操作时,始终应该检查操作是否成功,并且在不需要文件流时及时关闭文件。

sizeof、strlen、length/size的区别

strlen:
strlen 是 C 语言中的函数,用于计算字符串的长度(不包括字符串末尾的’\0’)。
仅适用于以 null 字符结尾的字符串,即 C-style 字符串。
返回值类型为 size_t。
length/size:
length 和 size 都是 C++ 中 string 类型的成员函数,用于返回字符串的长度
可以适用于任何字符串类型,包括 std::string 类型和 C-style 字符串类型。
返回值类型为 size_t。
在 C++ 中,std::string 的 length 和 size 成员函数不包含字符串末尾的 null 字符,因此它们返回的值是字符串的实际长度,不包括 null 字符。
sizeof:包括字符串末尾的’\0’
sizeof 是 C 和 C++ 中的操作符,用于返回其操作数的大小(以字节为单位)。
对于 C-style 字符串,sizeof 返回的是字符串数组的大小**,包括字符串末尾的 null 字符**,而不是字符串的长度。
对于 std::string 类型,sizeof 返回的是字符串对象本身的大小,而不是字符串的长度。
返回值类型为 size_t。

拷贝构造函数在什么时候会被用到

(1)当用类的一个对象去初始化该类的另一个对象时,系统会自动调用拷贝构造函数;
(2)将一个对象作为实参传递给一个非引用类型的形参,系统会自动调用拷贝构造函数;
(3)从一个返回类为非引用的函数返回一个对象时,系统会自动调用拷贝构造函数;
(4)用花括号列表初始化一个数组的元素时,系统会自动调用拷贝构造函数

std::move 的作用

将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者拷贝
image.png

构造函数=defualt是什么意思

没有参数的构造函数,将告诉编译器生成一个默认构造函数,它将初始化 ptrref_count 为默认值(通常为 nullptr 和 0

左值引用和右值引用

左值引用:给变量取别名,可以减少一层拷贝,左值是一个表示数据的表达式(变量名或解引用的指针)
右值引用:如
字面常量
表达式返回值函数返回值(不能是左值引用返回)等待,右值不能取地址
要区分左值与右值,只需要记住一个标准:
“左值可以取地址,右值不能被取地址”
。这就是左值和右值的本质区别。
因为左值一般是被放在地址空间中的,有明确的存储地址;而右值一般可能是计算中产生的中间值,也可能是被保存在寄存器上的一些值,总的来讲就是,右值并没有被保存在地址空间中,也就无法取地址

函数参数的在栈里面的调用方向

C/C++从右到左

值传递和引用传递

  1. 按值传递(pass by value):参数的值被复制给函数的形参,因此形参成为函数内的局部变量,其值类别将被视为左值。这意味着,无论传递给函数的参数是左值还是右值,形参都将成为函数内的左值。
  2. 按引用传递(pass by reference):参数以其原始的值类别传递给函数,如果参数是左值,形参将成为左值引用,如果参数是右值,形参将成为右值引用。这保留了原始参数的值类别。

在不使用完美转发的情况下,函数参数的传递方式决定了形参的值类别,因此会导致值类别丢失

void byValue(int x) {
    // x 是局部变量,因此它是左值
}

void byReference(int& x) {
    // x 是左值引用,它的值类别与传入参数相同
}

int main() {
    int a = 42;
    byValue(a);  // 传入左值,x 是左值
    byValue(123);  // 传入右值,x 仍然是左值

    int b = 56;
    byReference(b);  // 传入左值,x 是左值引用
    byReference(789);  // 传入右值,x 仍然是左值引用
    return 0;
}

完美转发

允许函数将其参数传递给另一个函数,同时保留原始参数的值类别(左值或右值)
使用完美转发可以提高代码的可复用性和性能,因为它允许函数传递参数给其他函数,而不会丢失参数的信息。这在实现通用函数、容器和类模板等情况下非常有用

template <typename T>
void forwarder(T&& arg) {
    target(std::forward<T>(arg)); // 使用 std::forward 进行完美转发
}

void target(int& x) {
    std::cout << "L-value reference: " << x << std::endl;
}

void target(int&& x) {
    std::cout << "R-value reference: " << x << std::endl;
}

int main() {
    int x = 42;
    forwarder(x);      // 调用 L-value reference 版本
    forwarder(123);    // 调用 R-value reference 版本
    return 0;
}

引用折叠

当引用与其他引用相遇或与引用与非引用相遇时,编译器会将它们合并成一个引用的规则

  1. 左值引用与左值引用相遇:两个左值引用合并成一个左值引用。
    • T& & 变成 T&
  2. 右值引用与右值引用相遇:两个右值引用合并成一个右值引用。
    • T&& && 变成 T&&
  3. 左值引用与右值引用相遇:一个左值引用和一个右值引用合并成一个左值引用。
    • T& && 变成 T&
template <typename T>
void foo(T&& t) {
    // 引用折叠:根据参数的值类别来选择合适的操作
    // 如果传入左值,t 是左值引用;如果传入右值,t 是右值引用
    t = 100; // 修改 t 对应的对象
}

int main() {
    int x = 42;
    foo(x); // x 是左值,t 是左值引用
    foo(123); // 123 是右值,t 是右值引用
    return 0;
}

C++迭代器怎么写,底层原理

正向,反向,双向

#include <iostream>

template <typename T>
class SimpleForwardIterator {
public:
    // 构造函数
    SimpleForwardIterator(T* ptr) : ptr_(ptr) {}

    // 解引用操作符
    T& operator*() const {
        return *ptr_;
    }

    // 前进操作
    SimpleForwardIterator& operator++() {
        ++ptr_;
        return *this;
    }

    // 后置递增操作
    SimpleForwardIterator operator++(int) {
        SimpleForwardIterator temp = *this;
        ++ptr_;
        return temp;
    }

    // 比较操作符
    bool operator!=(const SimpleForwardIterator& other) const {
        return ptr_ != other.ptr_;
    }

private:
    T* ptr_;
};

int main() {
    int arr[] = {1, 2, 3, 4, 5};

    // 创建一个简单的前向迭代器
    SimpleForwardIterator<int> begin(arr);
    SimpleForwardIterator<int> end(arr + 5);

    // 使用迭代器遍历数组
    for (SimpleForwardIterator<int> it = begin; it != end; ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

写一个迭代器

 unordered_map<int, string> myMap;
 //使用迭代器遍历无序映射
for (unordered_map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it) 
{ std::cout << "Key: " << it->first << ", Value: " << it->second << std::endl; }

迭代器什么时候会失效

1)插入和删除操作
2)删除容器元素
3)容器的重新分配
4)end()迭代器的失效

什么时候调用拷贝构造函数

1)使用一个对象去初始化同类型的另一个对象
2)以值传递的方式传递对象给函数
3)从函数返回一个对象
4)初始化容器元素,比如vector里面的push_back,emplace_back,都会调用对象的拷贝构造函数,但push_back得构造再拷贝,emplace_back是再容器中直接构造,如果enplace_back得时候容器中得内存正好被占用,会分配一个更大的内存块,然后把旧元素都拷贝过来

c++11的新特性有哪些

  1. 自动类型推导(auto):允许编译器根据初始化表达式的类型推导变量的类型,简化变量声明的过程。
  2. 统一的初始化语法:使用花括号({})进行初始化,可以用于初始化数组、结构体、类等各种类型的对象。
  3. 范围-based for 循环:提供了一种方便的方式来遍历容器(如数组、容器类等)中的元素,避免了使用迭代器的繁琐。
  4. Lambda 表达式:Lambda 表达式是一种匿名函数,可以在代码中内联定义和使用,方便编写简洁的函数对象和回调函数。
  5. Rvalue 引用和移动语义:引入了右值引用(Rvalue reference)和移动构造函数(Move constructor)和移动赋值运算符(Move assignment operator),使得对象的移动操作更高效,避免不必要的复制操作。
  6. 类型推导关键字(decltype):允许获取表达式的类型,可以用于声明变量、函数返回值的类型推导等场景。
  7. 空指针常量(nullptr):引入了一个空指针的新关键字,可以用于代替传统的NULL宏定义,增加了空指针类型的安全性。
  8. 右值引用和完美转发:通过右值引用和完美转发,可以实现对临时对象的引用,提高了代码的效率和性能。
  9. 智能指针:引入了两种智能指针,即shared_ptr和unique_ptr,用于管理动态分配的对象,避免内存泄漏和资源管理问题。
  10. 新的标准库组件:C++11引入了许多新的标准库组件,如正则表达式、原子操作、并发编程库等
    11:vector有了emplace_back

智能指针

可以避免程序员忘记手动释放内存,防止内存泄漏
早期C++98是auto_ptr,但是它赋值或拷贝控制所有权会转移
C++11里面引入unique_ptr,是独占式的,任意时刻只有一个unique_ptr可以指向相同的动态内存
禁用了拷贝构造函数,但不可以进行资源所有权转移了,非要转移得用move语义

#include<memory>intmain(){
std::unique_ptr<int> uniquePtr = std::make_unique<int>(42); 
   // 当 uniquePtr 超出作用域时,关联的内存将被自动释放return0; 
}

后面引入了shared_ptr,是共享式的,可以多个智能指针指向同一块内存,引用计数器来跟踪有多少个智能指针共享相同的对象,当最后一个共享指针被销毁的时候,关联对象才会被销毁

#include<memory>
intmain(){ 
  std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(42);
  std::shared_ptr<int> sharedPtr2 = sharedPtr1; 
  // 共享所有权// 当 sharedPtr1 和 sharedPtr2 超出作用域时,关联的内存将被自动释放return0;
}

但会产生循环依赖的问题

在后面就引入了week_ptr,可以解决循环依赖的问题

独占智能指针实现

template<typename T>
class MyUniquePtr{
public:
//构造
MyUniquePtr(T* ptr):ptr_=ptr{}

//禁用拷贝构造和拷贝赋值
MyUniquePtr(const MyUniquePtr&)=delete;
MyUniquePtr& oeprator=(const MyUniquePtr&)=delete;

//移动构造
MyUniquePtr(MyUniquePtr&&other){
    ptr_=other.ptr_;
    other.ptr_=nullptr;
}

//移动赋值
MyUniquePtr& oeprator=(MyUniquePtr&&other){
    if(this!=&other){
        delete ptr_;
        ptr_=other.ptr_;
        other.ptr_=nullptr;
    }
    return *this;
}

//获取指针
T*get(){
    return ptr_;
}

//解引用
T& operator*()const{
    return *ptr_;
}

//成员访问
T* operator->()const{
    return ptr_;
}

//析构
~MyUniquePtr(){
    delete ptr_;
}
private:
T* ptr_;
};

int main() {
    // 使用 MyUniquePtr 演示
    MyUniquePtr<int> uniquePtr(new int(42));
    std::cout << "Value: " << *uniquePtr << std::endl;

    // 移动构造函数
    MyUniquePtr<int> anotherPtr = std::move(uniquePtr);
    std::cout << "Value in anotherPtr: " << *anotherPtr << std::endl;

    // 移动赋值运算符
    MyUniquePtr<int> yetAnotherPtr(new int(99));
    yetAnotherPtr = std::move(anotherPtr);
    std::cout << "Value in yetAnotherPtr: " << *yetAnotherPtr << std::endl;

    return 0;
}

共享智能指针实现

#include <iostream>

template <typename T>
class SmartPtr {
public:
// 构造函数
explicit SmartPtr(T* ptr = nullptr) : ptr_(ptr), ref_count_(new size_t(1)) {}

// 拷贝构造函数
SmartPtr(const SmartPtr& other) : ptr_(other.ptr_), ref_count_(other.ref_count_) {
    ++(*ref_count_);
}

// 拷贝赋值运算符
SmartPtr& operator=(const SmartPtr& other) {
    if (this != &other) {
        // 减少旧引用计数
        decreaseRef();

        // 复制新指针和引用计数
        ptr_ = other.ptr_;
        ref_count_ = other.ref_count_;

        // 增加新引用计数
        ++(*ref_count_);
    }
    return *this;
}

//移动构造
SmartPtr(SmartPtr&& other) noexcept :ptr_(nullptr), ref_count_(nullptr) {
    swap(*this, other);
}

//移动赋值
SmartPtr& operator=(SmartPtr other) {
    swap(*this, other);
    return *this;
}

// 析构函数
~SmartPtr() {
    // 减少引用计数
    decreaseRef();
}

// 重载解引用运算符
T& operator*() const {
    return *ptr_;
}

// 重载箭头运算符
T* operator->() const {
    return ptr_;
}

friend void swap(SmartPtr& a, SmartPtr& b) {
    std::swap(a.ptr_, b.ptr_);
    std::swap(a.ref_count_, b.ref_count_);
}

private:
// 减少引用计数,可能释放内存
void decreaseRef() {
    if (ref_count_ != nullptr && --(*ref_count_) == 0) {
        delete ptr_;
        delete ref_count_;
    }
}

private:
    T* ptr_;          // 指向被管理对象的指针
    size_t* ref_count_;  // 引用计数
};


int main() {
    SmartPtr<int> ptr1(new int(42));
    SmartPtr<int> ptr2 = ptr1;  // 共享所有权

    std::cout << *ptr1 << std::endl;  // 42
    std::cout << *ptr2 << std::endl;  // 42

    *ptr1 = 10;  // 修改共享对象

    std::cout << *ptr1 << std::endl;  // 10
    std::cout << *ptr2 << std::endl;  // 10

    return 0;  // SmartPtr 在此处自动释放内存
}

智能指针引用计数器无法减一怎么办

  1. 循环引用:循环引用是指两个或多个对象之间相互持有对方的智能指针,导致引用计数器无法归零。这种情况下,智能指针的引用计数永远无法减到零,从而导致资源无法释放。解决循环引用的一种常见方法是使用weak_ptr来打破循环引用关系,弱引用不会增加对象的引用计数,避免了循环引用导致的计数无法减一的问题。
  2. 跨线程使用:如果在多线程环境下,智能指针被跨线程使用,可能会导致引用计数操作不正确,从而无法减一。在多线程环境中,对于引用计数器的操作需要进行同步,以避免竞争条件和数据访问冲突。可以使用互斥锁mutex或其他线程同步机制来保护引用计数的操作,确保线程安全。
  3. 非法使用:智能指针的正确使用方式是通过shared_ptr或unique_ptr等进行引用计数管理。如果不正确地使用普通指针或其他手动管理内存的方式来操作智能指针,可能会导致引用计数无法减一。确保在使用智能指针时遵循正确的使用方式,并避免手动管理智能指针所管理的对象。

unique_ptr 和shared_ptr的区别

1.** 所有权管理:**

  • unique_ptr:是独占所有权的智能指针,它拥有对动态分配对象的唯一所有权。当 unique_ptr被销毁或重置时,它所管理的对象也会被销毁。不能将同一个对象的所有权赋予多个unique_ptr
  • shared_ptr:是共享所有权的智能指针,它可以与其他 shared_ptr共享对同一个动态分配对象的所有权。通过引用计数的方式来管理资源,当最后一个 shared_ptr被销毁或重置时,对象才会被销毁。
  1. 内存管理开销:
    • unique_ptr:通常占用更少的内存,因为它只需要额外存储一个指针。不需要维护引用计数。
    • shared_ptr:需要维护引用计数,会有额外的开销,每个 shared_ptr都需要存储一个指针和一个计数器。
      3.** 所需语法:**
    • unique_ptr:使用移动语义,可以通过移动构造函数和移动赋值运算符来传递所有权,不能直接进行拷贝。
    • shared_ptr:可以进行拷贝和赋值操作,使用引用计数来管理对象的生命周期。
  2. 使用场景:
    • unique_ptr:适用于需要独占所有权的情况,例如在容器中存储对象或作为对象成员使用。它是一种轻量级的智能指针,没有引入额外的开销。
    • shared_ptr:适用于需要共享所有权的情况,例如多个对象共享同一个资源、循环引用的情况等。它提供了一种方便的方式来管理动态分配的资源。
      注意:shared_ptr的引用计数机制可能导致循环引用的问题,即两个或多个对象相互持有对方的 shared_ptr,导致资源无法释放。为了避免这种情况,可以使用 weak_ptr来打破循环引用。
      unique_ptr和 shared_ptr在所有权管理和内存管理开销上有所区别,根据不同的需求选择适合的智能指针类型。

shared_ptr的底层

  1. 指针管理:shared_ptr 内部包含一个指针,用于指向动态分配的对象。这个指针通常是通过 new 运算符分配内存得到的。指针管理部分负责在适当的时候释放对象的内存。
  2. 引用计数:shared_ptr 通过引用计数来跟踪有多少个 shared_ptr 实例共享同一个对象。引用计数通常以整数形式存在,并存储在与所指对象相关联的控制块(control block)中。
  3. 控制块(Control Block):控制块是 shared_ptr 内部的一个数据结构,用于存储引用计数和其他相关信息。控制块通常是动态分配的,并与所指对象共享。
  4. 内存管理:当创建一个 shared_ptr 时,它会与一个控制块关联,并将指针指向所管理的对象。控制块中的引用计数会被初始化为1。当创建其他 shared_ptr 实例并与同一个对象关联时,引用计数会递增。当 shared_ptr 被销毁或重置时,引用计数会递减。当引用计数为0时,表示没有 shared_ptr 实例与该对象关联,此时控制块会负责释放对象的内存。
  5. 线程安全:shared_ptr 的默认实现是线程安全的,它使用原子操作来确保引用计数的并发访问安全。这样可以在多线程环境下安全地共享对象。

shared_ptr什么时候会改变它的引用计数?

  1. 初始化和拷贝:当创建 shared_ptr 对象时,引用计数会初始化为1。当将一个 shared_ptr 对象赋值给另一个 shared_ptr 对象或进行拷贝构造时,引用计数会增加
std::shared_ptr<int> ptr1(new int(5));  // 引用计数为1
std::shared_ptr<int> ptr2 = ptr1;       // 引用计数加1
  1. 重置和赋值:通过 reset 函数或赋值操作符 =shared_ptr 重新指向新的对象,会导致引用计数的变化。旧的对象引用计数减少,新的对象引用计数增加
std::shared_ptr<int> ptr(new int(5));    // 引用计数为1
ptr.reset(new int(10));                  // 引用计数减少到0,释放旧对象;引用计数变为1,指向新对象
  1. 解除引用:当 shared_ptr 超出作用域或通过 reset 函数将其置空时,引用计数减少。当引用计数变为0时,内部资源会被释放,即对象会被删除。
std::shared_ptr<int> ptr(new int(5));  // 引用计数为1
// 引用计数减少到0,内部对象被删除
  1. 手动修改引用计数:虽然不推荐,但可以通过 shared_ptr的别名获取底层引用计数指针,并手动修改引用计数。但要谨慎操作,确保引用计数的正确性。
    shared_ptr的引用计数会在对象的初始化、拷贝、重置、赋值和解除引用等操作中发生变化。引用计数的准确管理是 shared_ptr能够自动释放对象资源的关键机制。

拷贝构造函数引用计数会变化吗,赋值会改变吗,哪边变化?

在拷贝构造函数中,新创建的 shared_ptr 引用计数会增加;
在赋值操作符中,目标 shared_ptr 的引用计数会增加,而源 shared_ptr(如果有)的引用计数会减少

unique_ptr指向2个对象,什么时候调用析构函数

unique_ptr 是一种独占所有权的智能指针,一个 unique_ptr 对象可以拥有对一个动态分配对象的唯一所有权。当 unique_ptr 被销毁或重置时,它所管理的对象会被析构。
如果一个 unique_ptr 指向两个不同的对象,例如通过移动语义或者重新分配 unique_ptr 的所有权,那么当 unique_ptr 被销毁或重置时,只会调用其中一个对象的析构函数
具体来说,当 unique_ptr 被销毁或重置时,它会检查当前是否存在一个有效的指针(即指向一个对象)。如果存在有效的指针,那么会调用该对象的析构函数来销毁它。对于另一个对象,由于它的所有权已经转移到其他 unique_ptr 或已被释放,所以不会调用其析构函数
需要注意的是,在同一个 unique_ptr 对象上进行多次重置会导致其之前指向的对象被销毁多次,这是一种未定义行为。确保在重置 unique_ptr 之前,将其指向的对象正确释放或移交给其他 unique_ptr 或智能指针。
总结:unique_ptr 只能拥有对一个对象的唯一所有权,当 unique_ptr 被销毁或重置时,只会调用其中一个对象的析构函数。

shared_ptr指向2个对象,什么时候调用析构函数

shared_ptr 是一种共享所有权的智能指针,它可以与其他 shared_ptr 共享对同一个动态分配对象的所有权。当最后一个 shared_ptr 对象被销毁或重置时,才会调用所管理对象的析构函数。
如果一个 shared_ptr 指向两个不同的对象,即多个 shared_ptr 共享对这两个对象的所有权,那么当最后一个 shared_ptr 被销毁或重置时,才会调用这两个对象的析构函数
shared_ptr 内部通过引用计数来跟踪共享的对象,并在引用计数为0时释放对象的内存。每个 shared_ptr 对象都有一个关联的控制块(control block),其中包含引用计数等信息。当一个新的 shared_ptr 对象与现有的对象关联时,引用计数会增加当一个 shared_ptr 对象被销毁或重置时,引用计数会减少。只有当引用计数降为0时,才会调用对象的析构函数
因此,只有当最后一个 shared_ptr 对象与这两个对象的所有权解除关联时,也就是引用计数降为0时,才会调用这两个对象的析构函数。其他 shared_ptr 对象的销毁或重置不会触发析构函数的调用,只会减少引用计数。
需要注意的是,使用 shared_ptr 时要注意避免循环引用,即两个或多个对象相互持有对方的 shared_ptr,这会导致对象无法释放,内存泄漏的问题。可以使用 weak_ptr 打破循环引用,以便正确释放对象。

unique_ptr在函数内使用,指向的是在堆区还是栈区

unique_ptr 可以指向在堆区(动态分配)的对象,而不能直接指向栈区(自动分配)的对象。
在函数内部创建一个 unique_ptr 并使用 new 运算符来分配内存,那么它将指向堆区的对象。这是因为 unique_ptr 是专门用于管理动态分配的对象的智能指针。在函数执行完毕unique_ptr 被销毁时,它会自动释放所管理的堆区对象
例如,下面的示例演示了在函数内部使用 unique_ptr 来管理动态分配的对象:

#include <memory>

void foo() {
    std::unique_ptr<int> ptr(new int(10));  // 指向堆区的对象
    // 使用 ptr
    // ...
}  // 在函数结束时,ptr 被销毁,堆区对象也会被自动释放

unique_ptr ptr 在函数内部被创建,并通过 new 运算符在堆区分配了一个整型对象。当函数 foo 结束时,ptr 被销毁,它的析构函数会自动释放堆区对象。
如果你尝试将 unique_ptr 直接指向栈区对象,会导致编译错误。因为 unique_ptr 的析构函数会尝试删除(delete)指向的对象,而栈区对象在函数结束时会自动被销毁,不能通过 delete 来释放。
因此,一般情况下,unique_ptr 用于管理在堆区分配的对象,而不是栈区的对象。如果需要管理栈区对象,可以使用原生指针或其他适当的智能指针(如 std::shared_ptr)。

shared_ptr在函数内使用,指向的是在堆区还是栈区

shared_ptr 可以指向在堆区(动态分配)的对象,也可以指向栈区(自动分配)的对象。它并不关心对象是在堆区还是栈区分配的,而是关心对对象的所有权管理。
当你在函数内部创建一个 shared_ptr 并将其指向堆区对象时,它将管理堆区对象的所有权。当最后一个持有该对象的 shared_ptr 被销毁时,堆区对象将被自动释放。
当你在函数内部创建一个 shared_ptr 并将其指向栈区对象时,它同样可以管理栈区对象的所有权。但需要注意的是,当函数结束时,栈区对象会被自动销毁,而 shared_ptr 的析构函数会尝试释放对象,这可能导致未定义行为。因此,为了避免这种情况,如果你使用 shared_ptr 管理栈区对象,需要在 shared_ptr 被销毁之前手动释放或者重置它。

#include <memory>
void foo() {
    // 在堆区分配对象
    std::shared_ptr<int> ptr1(new int(10));  // 指向堆区的对象
    // 在栈区分配对象
    int value = 20;
    std::shared_ptr<int> ptr2(&value, [](int*) { });  // 指向栈区的对象,自定义删除器为空
    // 使用 ptr1 和 ptr2
    // ...
}  // 在函数结束时,ptr1 和 ptr2 被销毁,堆区对象会自动释放,栈区对象不会被释放

在上述示例中,ptr1 是指向堆区分配的整型对象的 shared_ptrptr2 是指向栈区的整型对象的 shared_ptr,使用了一个自定义的删除器,但这里删除器为空,因为栈区对象不需要被释放。
需要注意的是,当你使用 shared_ptr 管理栈区对象时,确保在 shared_ptr 对象被销毁之前,不要访问已经销毁的栈区对象。并且要避免在多个 shared_ptr 之间共享对栈区对象的所有权,以防止悬空指针的问题。一般情况下,shared_ptr 更适合用于管理动态分配的堆区对象。

当智能指针对象使用完后**,对象就会自动调用析构函数去释放该指针所指向的空间**

智能指针是C++中用于管理动态分配内存的工具,它们提供了自动化的内存管理,可以避免常见的内存泄漏和悬挂指针等问题。以下是几种常见的智能指针及其作用:

  1. unique_ptr:unique_ptr是独占式智能指针,它确保在任何时候只有一个unique_ptr实例可以拥有对特定资源的所有权。当unique_ptr超出范围或被删除时,它会自动释放所拥有的资源。这种智能指针非常适合管理单个动态分配的对象或数组。
  2. shared_ptr:shared_ptr是共享式智能指针,它允许多个shared_ptr实例共享对同一资源的所有权。它使用引用计数技术来跟踪资源的使用情况,并在不再有任何shared_ptr实例引用资源时释放资源。shared_ptr适用于需要共享动态分配的对象或资源的情况,可以防止资源过早释放或悬挂指针的问题。
  3. weak_ptr:weak_ptr也是一种共享式智能指针,但它不会增加引用计数。weak_ptr通常与shared_ptr一起使用,用于解决循环引用的问题。循环引用指两个或多个对象彼此持有shared_ptr,并因此无法释放。通过使用weak_ptr,可以在需要时获取shared_ptr的临时拷贝,而不会增加引用计数,从而打破循环引用。
    这些智能指针的作用是确保动态分配的内存资源在不再使用时能够正确释放,从而提高程序的内存安全性和可靠性。它们减少了手动管理内存的工作量,避免了内存泄漏和悬挂指针等常见错误。使用智能指针可以简化代码,并提高代码的可读性和可维护性。然而,需要注意避免循环引用的问题,以免导致资源泄漏。

如果智能指针在函数里使用,会在什么时候释放

智能指针的释放时机取决于其作用域和所有权的转移。

  1. unique_ptr:当unique_ptr超出其作用域时(例如函数结束、代码块结束),或者通过std::move将所有权转移给其他unique_ptr时,unique_ptr将自动释放所管理的资源。这意味着在函数内部使用unique_ptr时,当函数返回或代码块结束时,unique_ptr将自动释放所拥有的资源
  2. shared_ptr:shared_ptr使用引用计数来跟踪资源的使用情况。当最后一个引用到shared_ptr的实例超出作用域时,即没有任何shared_ptr实例引用该资源时,引用计数会变为零,资源将被释放。因此,在函数内部使用shared_ptr时,只有在最后一个引用离开作用域时,资源才会被释放
  3. weak_ptr:weak_ptr本身并不拥有资源,它只是观察shared_ptr的生命周期。当最后一个shared_ptr超出作用域时,资源被释放,无论是否有weak_ptr观察。因此**,weak_ptr不直接参与资源的释放**。
    总结起来,无论是unique_ptr还是shared_ptr,在函数内部使用时,智能指针的释放时机取决于其作用域和所有权的转移。一般情况下,当智能指针超出其作用域时,或者将所有权转移到其他智能指针时,资源将被自动释放。这种自动释放的机制可以有效地避免内存泄漏和资源管理的问题。

智能指针

为什么要使用智能指针

1)帮C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏
2)new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
3)使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!
4)在动态内存管理中,使用智能指针可以避免手动管理内存的麻烦和出错风险。

智能指针原理:解决指针自动释放

分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存

  1. C++98 提供了 auto_ptr 模板的解决方案
  2. C++11 增加unique_ptr、shared_ptr 和weak_ptr

auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

为什么智能指针可以像普通指针那样使用

因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象

智能指针的三个常用函数
  1. get() 获取智能指针托管的指针地址
  2. release() 取消智能指针对动态内存的托管
  3. reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
使用建议
  1. 尽可能不要将auto_ptr 变量定义为全局变量或指针;
  2. 除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个 智能指针;
  3. C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。
  4. auto_ptr 被C++11抛弃的主要原因

1)复制或者赋值都会改变资源的所有权
2)在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
3)不支持对象数组的内存管理

unique_ptr

C++11用更严谨的unique_ptr 取代了auto_ptr!
unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。

unique_ptr特性
  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的
  5. 在 STL 容器中使用unique_ptr,不允许直接赋值
vec[0]= vec[1];	/* 不允许直接赋值 */ 
vec[0]= std::move(vec[1]);// 需要使用move修饰,使得程序员知道后果

shared_ptr

  1. 由于auto_ptr 与 unique_ptr的排他性,可以使用shared_ptr指针
  2. 熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?
  3. 有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!
shared_ptr使用陷阱

shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!
要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!
解决方法:使用weak_ptr弱指针。

weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。

智能指针的使用陷阱

  1. 不要把一个原生指针给多个智能指针管理;
  2. 记得使用u.release()的返回值;

在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.

  1. 禁止delete 智能指针get 函数返回的指针;

如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!

  1. 禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!

如果遇到内存泄漏这种问题,你一般是怎么去解决

  • 在程序中加入必要的错误处理代码,避免程序因为异常情况而导致内存泄漏。
  • 使用智能指针等RAII机制,自动管理内存,避免手动管理内存的麻烦和出错风险。
  • 使用内存分析工具,检测程序中的内存泄漏,并进行相应的修复。

lambda表达式

lamda表达式[this](){},this是捕获值还是引用

捕获当前对象的引用。这允许 Lambda 表达式在其函数体中访问当前对象的成员变量和成员函数
修改外部作用域的变量要加mutable关键字

说一下匿名函数

Lambda函数,是一种在编程语言中用于表示内联函数或匿名函数的特殊语法结构。它允许我们定义一个临时的、无需命名的函数,通常用于需要在某个特定的上下文中定义和使用函数的场景。
在C++中,Lambda函数是通过Lambda表达式来创建的。Lambda表达式具有以下一般形式:

//capture list是用于捕获变量的列表,
//parameters是函数参数
//return_type是返回类型
//函数体是实际的函数代码
[capture list](parameters) -> return_type {
    // 函数体
}
#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    // Lambda函数示例
    auto sum = [x, &y](int a, int b) -> int {
        y = 30;  // 修改外部变量y
        return a + b + x + y;
    };

    int result = sum(5, 7);
    std::cout << "Result: " << result << std::endl;  // 输出:Result: 52

    return 0;
}

上述示例中的Lambda函数通过捕获变量x和引用捕获变量y,计算了两个参数的和,并访问了外部的x和y变量。Lambda函数的返回类型被指定为int。
Lambda函数是一种方便的方式来定义匿名的、临时的函数,提供了更灵活和简洁的代码编写方式,特别适用于需要在特定上下文中定义和使用函数的场景。

lambda 表达式的优势是什么

  1. 简洁性和可读性:Lambda表达式允许在需要时直接定义函数,无需显式命名函数,从而减少了代码的冗余和噪声。它可以将函数的定义与调用紧密地放在一起,使代码更加紧凑和易读。
  2. 匿名性:Lambda表达式是匿名的,无需给函数命名,因此非常适合于临时或只在特定上下文中使用的函数。它可以在需要时直接在代码中定义和使用,无需创建额外的函数对象。
  3. 闭包功能:Lambda表达式可以捕获其定义范围之外的变量,形成闭包。这使得Lambda函数可以访问和操作外部的变量,从而方便地实现一些需要访问外部状态的功能。
  4. 灵活性:Lambda表达式可以具有自定义的参数列表和返回类型,因此可以根据需要定义各种类型的函数。它可以接受任意数量和类型的参数,并返回任意类型的值,使其非常灵活和适应不同的编程需求。
  5. 与标准库的结合:Lambda表达式广泛应用于C++标准库的算法和容器操作中,例如STL的for_each、sort、transform等函数,以及容器的遍历和筛选操作。通过Lambda表达式,可以以更简洁的方式传递自定义的操作或谓词,提高代码的可读性和可维护性。
  6. **函数对象的替代:**Lambda表达式可以用作函数对象的替代,避免了显式定义和实例化函数对象的繁琐过程。它使得编写一次性的、只用于特定场景的函数更加方便。

Lambda用法

匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
[ caputrue ] ( params ) opt -> ret { body; };

  1. capture是捕获列表;
  2. params是参数表;(选填)
  3. opt是函数选项;可以填mutable,exception,attribute(选填)
    mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
    exception说明lambda表达式是否抛出异常以及何种异常。
    attribute用来声明属性。
  4. ret是返回值类型(拖尾返回类型)。(选填)
  5. body是函数体。
    捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
  6. []不捕获任何变量。
  7. [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  8. [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。
使用lambda进行sort排序
vector<int>vec;
sort(vec.begin(), vec.end(),[](int a,int b){return a > b;});

模板

template 模版你是怎么理解的

模板(Template)是C++中的一种强大的泛型编程工具,它允许编写通用的代码,能够适用于多种不同的数据类型。模板通过参数化类型和参数化值来定义通用的函数、类、数据结构等。
模板可以分为函数模板和类模板两种形式。
1**. 函数模板:**

  • 函数模板允许定义一个通用的函数,可以处理不同类型的参数。
  • 函数模板使用参数化类型来代替具体的类型,在使用时根据实际需要进行类型推导。
  • 函数模板的定义以关键字template开始,后面跟着模板参数列表和函数的定义。
  • 例如,下面是一个简单的函数模板,用于交换两个值:
 template <typename T>
 void swapValues(T& a, T& b) {
     T temp = a;
     a = b;
     b = temp;
 }

2. 类模板:

  • 类模板允许定义一个通用的类,可以处理不同类型的成员和操作。
  • 类模板使用参数化类型来代替具体的类型,在实例化时根据实际需要进行类型推导。
  • 类模板的定义以关键字template开始,后面跟着模板参数列表和类的定义。
  • 例如,下面是一个简单的类模板,表示一个通用的栈数据结构:
template <typename T>
class Stack {
private:
 std::vector<T> data;
public:
 void push(const T& item) {
     data.push_back(item);
 }

 T pop() {
     T item = data.back();
     data.pop_back();
     return item;
 }
};

使用模板的好处包括:

  • 提高代码的复用性:模板可以根据不同的类型生成具体的代码,避免了为每种类型都编写相似的代码。
  • 提供更高的灵活性:模板允许根据实际需求进行类型参数化,适用于多种不同的数据类型。
  • 支持泛型编程:模板是实现泛型编程的重要工具,可以编写更加通用、灵活和高效的代码。
    需要注意的是,模板在编译时进行实例化,生成具体的代码,因此在使用模板时需要确保模板代码的正确性。同时,模板的错误信息可能会较为复杂,需要注意调试和处理模板相关的编译错误。

模版的底层是怎么实现的

模板的底层实现是通过编译器在编译时进行代码生成和实例化编译器根据模板定义和使用的上下文,在编译阶段将模板代码生成具体的代码
具体而言,编译器在遇到模板定义时,并不会立即生成代码,而是将模板代码存储在编译器的符号表中,并对其进行语法和语义的检查。当编译器遇到模板的实例化(即模板被使用)时,它会根据实际的模板参数,生成对应的具体代码。
实例化模板的过程包括两个主要的步骤:

  1. 模板参数推导(Template Argument Deduction)
    • 当编译器遇到模板的实例化时,会根据实际的参数类型,推导出模板参数的具体类型。
    • 推导的过程中,编译器会检查参数的类型,并尝试匹配模板参数的类型,确保类型的一致性。
    • 如果推导失败,编译器会产生编译错误。
  2. 代码生成:
    • 一旦模板参数被推导出,编译器就会根据具体的模板参数,生成对应的代码。
    • 生成的代码会替换模板定义中的模板参数,并展开成实际的代码。
    • 生成的代码可以在编译时进行优化和静态检查,提供更好的性能和安全性。
      需要注意的是,模板的实例化是延迟到使用时进行的,即模板的代码只有在实际使用时才会被编译器实例化和生成。这种按需生成代码的机制使得模板具有高度的灵活性和可扩展性。
      模板的底层实现还涉及到模板的链接问题。由于模板通常定义在头文件中,可能会被多个源文件包含和使用,因此需要注意模板的链接规则和避免重复定义。

模板说一下

C++模板是一种通用编程机制,允许您编写可以处理多种数据类型的代码,而无需为每种数据类型都编写特定的代码。模板是C++中强大而灵活的功能,主要包括类模板和函数模板。

//类模板的比较函数
template<typename T>
class Compare{
public:
   //比较函数
   T max(T a,T b){
      return (a>b)?a:b;
   }

};

//函数模板
template<typename T>
T max(T a,T b){
  return (a>b)?a:b;
}

模板的优缺点

:::info
C++模板是一种强大的编程工具,但它们也有优点和缺点,取决于如何使用和实现。以下是模板的一些主要优点和缺点:

优点

  1. 通用性:模板允许编写通用代码,可以处理多种数据类型,从而提高了代码的重用性。这使得模板在编写容器、算法和数据结构等通用库时非常有用
  2. 类型安全:使用模板可以在编译时检查类型,减少了在运行时发生类型错误的可能性。
  3. 高效性:模板生成的代码通常在编译时展开,因此不会引入运行时开销。这使得模板生成的代码通常比宏展开更高效。
  4. 泛型编程:模板支持泛型编程,允许编写高度抽象的代码,可以处理各种数据类型和数据结构。
  5. 标准库支持:C++标准库中许多功能都基于模板构建,包括STL容器、算法和迭代器等。

缺点

  1. 编译时错误信息复杂:由于模板生成的代码在编译时展开,错误消息可能会变得复杂和难以理解,尤其是当涉及到模板的嵌套和复杂类型时。
  2. 代码可读性:使用过多的模板元编程技巧和复杂的模板特化可能会导致代码难以阅读和维护。
  3. 编译时间:包含大量模板的项目可能会导致较长的编译时间,尤其是当使用大型代码库时。
  4. 不适合所有情况:模板不是解决所有问题的银弹。有时,使用非模板的特定实现更简单和高效。
  5. 复杂性:模板编程有时会引入复杂性,特别是在实现高级模板技术时,如SFINAE(Substitution Failure Is Not An Error)和模板特化。
    总的来说,模板是一种强大的工具,可用于构建通用和高性能的代码。但它们也需要谨慎使用,以避免引入过度复杂性和编译时错误。正确使用模板可以提高代码的可维护性和效率,但需要慎重考虑何时使用和何时不使用它们。
    :::

模板比较大小

#include<iostream>
using namespace std;

//函数模板
template<typename type1,typename type2>
type1 Max(type1 a,type2 b){
   return a > b ? a : b;
}
int main(){
  cout<<"Max = "<<Max(5.5,4.5)<<endl;
  return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据您提供的Makefile,这个错误信息是由于在删除目标文件时出现问题引起的。这可能是由于您的Dev-C++环境配置不正确导致的。 在您的Makefile中,您使用了`$(DEL)`变量来执行删除操作。然而,在Windows环境下,删除操作通常使用`del`命令而不是`devcpp.exe INTERNAL_DEL`。 为了解决这个问题,您可以尝试将以下行: ``` DEL = C:\Program Files (x86)\Embarcadero\Dev-Cpp\devcpp.exe INTERNAL_DEL ``` 替换为: ``` DEL = del ``` 这将使用Windows的`del`命令来执行删除操作。 修改后的Makefile如下所示: ```makefile # Project: 项目1 # Makefile created by Embarcadero Dev-C++ 6.3 CPP = g++.exe CC = gcc.exe WINDRES = windres.exe OBJ = main.o LINKOBJ = main.o LIBS = -L"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib" -L"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/lib" -static-libgcc INCS = -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include" CXXINCS = -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++" BIN = 项目1.exe CXXFLAGS = $(CXXINCS) -std=c++11 CFLAGS = $(INCS) -std=c++11 DEL = del .PHONY: all all-before all-after clean clean-custom all: all-before $(BIN) all-after clean: clean-custom ${DEL} $(OBJ) $(BIN) $(BIN): $(OBJ) $(CPP) $(LINKOBJ) -o $(BIN) $(LIBS) main.o: main.cpp $(CPP) -c main.cpp -o main.o $(CXXFLAGS) ``` 请尝试使用修改后的Makefile重新编译您的项目,看看是否能够解决问题。如果还有其他错误信息,请提供详细的错误信息,以便我更好地帮助您解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值