C++学习2

2、CPP 基础语法学习 很多语法点从汇编角度分析就很明了了 加油一、基础语法 A1、cin、cout2、函数重载(Overload)3、默认参数4、extern "C"C 和 C++混合开发二、基础语法 B5、内联函数 (inline function)窥探内敛函数的本质:内联函数与宏表达式6、 const7、引用(Reference)引用的本质三、汇编语言1、学习汇编指令的 2 大知识点:2、内联汇编3、汇编指令mov 指令call 指令lea指令(指针)ret、add、sub、inc、dec、xorjmp常见代码的汇编4、. 阅读详情

小林C++八股文

一、C++基础

什么是指针?指针的大小及用法?

指针: 指向另外一种类型的复合类型。 指针的大小: 在 64 位计算机中,指针占 8 个字节空间。
指针5种 指向普通对象的指针、常量指针、函数指针、this 指针

什么是野指针和悬空指针?

悬空指针: 若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。
“野指针”是指不确定其指向的指针,未初始化的指针为“野指针”。

指针和引用的区别是什么?

  • 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
  • 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。(是否占内存)
  • 指针可以为空,但是引用必须绑定对象。(是否可为空)
  • 指针可以有多级,但是引用只能一级。(是否能为多级)

常量指针和指针常量的区别是什么?

左定值右定向

函数指针和指针函数的区别是什么?

  • 函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。
  • 指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。

指针和引用值传递的区别是什么?

  • 值传递是指将参数的值复制一份,传递给函数进行操作。在值传递中,函数对参数进行修改不会影响到原始的变量值。
  • 指针引用是指将参数的内存地址传递给函数或方法,使得函数或方法可以直接访问和修改原始变量的值。在指针引用中,函数或方法对参数的修改会直接反映在原始变量上。

函数传参用引用的作用是什么?

避免拷贝,使用引用传参可以避免对大型对象进行复制

参数传递时,值传递、引用传递、指针传递的区别?

  • 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
  • 指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
  • 引用传递:当形参是引用类型时,可以直接访问到原始值。

C++全局变量、局部变量、静态全局变量、静态局部变量的区别?

从作用域看:

  • 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern关键字再次声明这个全局变量。
  • 静态全局变量:具有文件作用域。
  • 局部变量:具有局部作用域。
  • 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

从分配内存空间看:

  • 静态存储区:全局变量,静态局部变量,静态全局变量。
  • 栈:局部变量。

全局变量定义在头文件中有什么问题?

如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。

extern C 的作用是什么?

当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern “C” 指出任意非 C++ 函数所用的语言。

sizeof(1==1) 在 C 和 C++ 中分别是什么结果?

两者结果不一样,精度是不一样的
c语言中,sizeof(int) 输出4
c++中,sizeof(bool)输出1

C 和 C++ struct 的区别?

  • 在 C 语言中 struct 是用户自定义数据类型,需要加上struct关键字;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义,不用加关键字。
  • C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct可以和类一样,有访问权限,并可以定义成员函数。

C++ 中 struct和Class区别是什么?

主要体现在默认访问权限和默认继承方式上:

  • 默认访问权限不同:struct 默认访问权限为 public,class 默认访问权限为 private
  • 默认继承方式不同:当使用继承时,两者的默认继承权限也不同,struct 默认继承方式为 public继承,class 默认继承方式为private 继承。

为什么有了 class 还保留 struct?

C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。

struct 和 union 的区别是什么?

  • 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
  • 对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。
  • 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。

class 和 struct 的异同是什么?

  • struct 和 class 都可以自定义数据类型,也支持继承操作。
  • struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是private,默认的继承级别也是 private。
  • 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct本身,class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。

C++ static作用是什么?

static 定义静态变量,静态函数。

  • 保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期
  • 隐藏:static作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。
  • static作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。
    注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。

static 在类中使用的注意事项有哪些?

static 静态成员变量:

  • 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现static关键字和private、public、protected访问规则。
  • 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。
  • 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。

static 静态成员函数:

  • 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。
  • 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。

static 全局变量和普通全局变量的异同是什么?

相同点:

  • 存储方式:普通全局变量和 static 全局变量都是静态存储方式。

不同点:
作用域:

  • 普通全局变量的作用域是整个源程序;静态全局变量只在定义该变量的源文件内有效
  • 初始化:静态全局变量只初始化一次,防止在其他文件中使用。

C++ 静态变量的使用场景是什么?未初始化的全局静态变量呢?

全局静态变量(static 修饰的全局变量):

  • 当多个文件需要独立使用同名变量(如统计各模块的内部计数),但不希望被其他文件访问或修改时。

局部静态变量(函数内的 static 变量):

  • 记录函数被调用的次数
  • 单例模式中,确保全局只存在一个实例

类静态成员变量:

  • 统计类的实例数量如 static int total;,在构造函数中 total++,析构函数中 total–

未初始化的全局静态变量:

  1. 自动初始化:编译器会将其默认初始化为 0(包括数值类型为 0,指针类型为 nullptr 等)。 这是因为全局静态变量存放在内存的BSS 段(未初始化数据段),程序启动时系统会自动将该段所有数据清零。
  2. 作用域限制:和初始化的全局静态变量一样,仅在当前文件内可见,不影响其他文件的同名变量。

define 和 const 的区别是什么?

区别:

  • 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。
  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。
  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const定义的常量占用静态存储区的空间,程序运行过程中只有一份。
  • 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;cons定义的常量可以进行调试。

const 的优点:

  • 有数据类型,在定义式可进行安全性检查。
  • 可调式。
  • 占用较少的空间。

define 和 typedef 的区别是什么?

原理:

  • define作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。

功能:

  • typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。

作用域:

  • define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。

指针的操作:

  • typedef 和 #define 在处理指针时不完全一样

volatile 的作用?是否具有原子性,对编译器有什么影响?

  • volatile 的作用:当对象的值可能在程序的控制之外被改变时,应该将该对象声明为violatile,告知编译器不应对这样的对象进行优化。
  • volatile不具有原子性。
  • volatile对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,也有可能使用寄存器中的变量,从而导致程序错误。

为什么一般将析构函数设置为虚函数?

析构函数被设为虚函数主要是为了解决基类指针指向派生类对象时的资源释放问题。

如果我们有一个基类指针,它实际上指向一个派生类对象,当我们删除这个基类指针时,如果析构函数不是虚函数,那么就只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致派生类对象的一些资源没有被正确释放,从而引发内存泄漏等问题。

如果我们将析构函数设置为虚函数,那么在删除基类指针时,会首先调用派生类的析构函数,然后再调用基类的析构函数,从而确保所有的资源都能被正确释放。

内联函数有什么缺点?

内联函数的缺点主要有以下几点:

  • 代码膨胀:内联函数会在每个调用它的地方进行代码替换,这可能导致代码膨胀。如果内联函数体非常大或者被频繁调用,会增加可执行文件的大小,可能导致缓存不命中,影响性能。
  • 编译时间增加:内联函数需要在每个调用点进行代码替换,这会增加编译时间。特别是当内联函数被广泛使用时,编译时间可能会显著增加。
  • 可读性降低:内联函数会将函数体嵌入到调用点,可能导致代码的可读性降低。函数体被分散在多个地方,可能会使代码难以理解和维护。

include “ “ 和 <> 的区别是什么?

include<文件名> 和 #include”文件名” 的区别:

  • 查找文件的位置:include<文件名>在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;#include”文件名”在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。

• 使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用 #include”文件名”

什么是内联函数?

在C++中,使用关键字"inline"可以声明一个内联函数。声明为内联函数的函数会在编译时被视为候选项,编译器会尝试将其展开,将函数体直接插入到调用点处。这样可以避免函数调用的开销,减少了函数调用的栈帧等额外开销,从而提高程序的执行效率。

sizeof 和 strlen 的区别是什么?

strlen 是头文件 中的函数,sizeof 是 C++ 中的运算符。 strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。而 sizeof 测量的是字符数组的分配大小。

strlen 源代码:
size_t 
strlen(const char *str) {
    size_t length = 0;
    while (*str++)
        ++length;
    return length;
}
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    char arr[10] = "hello";
    cout << strlen(arr) << endl; // 5
    cout << sizeof(arr) << endl; // 10
    return 0;
}

若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以看出。

sizeof(arr):作用于数组 如果 arr 被定义为一个固定大小的数组,例如 char arr[10];,那么sizeof(arr) 会在编译时计算出整个数组占用的内存大小。

  • 因为 char 类型的大小固定为 1 字节。
  • 数组 arr 包含 10 个 char 元素。
  • 所以 sizeof(arr) 的结果是 10 * 1 = 10 字节。

sizeof(ptr):作用于指针,如果你有一个指向字符的指针,例如 char *ptr;,那么 sizeof(ptr) 返回的是指针变量 ptr 本身在内存中占用的空间大小。

  • 在64位系统上,内存地址的大小通常是 8 字节。
  • 所以 sizeof(ptr) 的结果是 8。
#include <iostream>
#include <cstring>

using namespace std;

void size_of(char arr[])
{
    cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .
    cout << strlen(arr) << endl; 
}

int main()
{
    char arr[20] = "hello";
    size_of(arr); 
    return 0;
}
/*
输出结果:
8
5
*/
  • strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度;
  • sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char* 类型的变量。

memcpy 函数的底层原理是什么?

memcpy 函数的底层原理简单说就是直接操作内存块的二进制数据。它会从源地址开始,逐个字节(或按更高效的块)复制数据到目标地址,直到复制完指定的字节数。

底层实现通常会做优化,比如对对齐的内存块用更大的单位(如 4 字节、8 字节)批量复制,比单字节循环更快;对未对齐的部分先用单字节处理到对齐位置,再用块复制。整个过程不关心数据类型,纯粹按字节搬运,所以复制后目标内存和源内存的二进制内容完全一致,但不会处理像字符串结束符这类特殊情况。

void *memcpy(void *dst, const void *src, size_t size)
{
    char *psrc;
    char *pdst;

    if (NULL == dst || NULL == src)
    {
        return NULL;
    }

    if ((src < dst) && (char *)src + size > (char *)dst) // 出现地址重叠的情况,自后向前拷贝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while (size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while (size--)
        {
            *pdst++ = *psrc++;
        }
    }

    return dst;
}

strcpy 函数有什么缺陷?

strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。

#include <iostream>
#include <cstring>
using namespace std;

int main()
{
    int var = 0x11112222;
    char arr[10];
    cout << "Address : var " << &var << endl;
    cout << "Address : arr " << &arr << endl;
    strcpy(arr, "hello world!");
    cout << "var:" << hex << var << endl; // 将变量 var 以 16 进制输出
    cout << "arr:" << arr << endl;
    return 0;
}

/*
Address : var 0x23fe4c
Address : arr 0x23fe42
var:11002164
arr:hello world!
*/

二、C++编译器

C++的编译过程介绍一下?

在这里插入图片描述

  • 预处理:预处理阶段会对源代码进行处理,主要包括展开宏定义、处理条件编译指令(如#include、#define、#ifdef等)以及删除注释等。预处理的结果是生成一个经过宏展开和条件处理后的纯C++源代码文件。
  • 编译(Compilation):编译阶段将预处理后的源代码翻译为汇编语言,生成汇编代码。编译器会进行词法分析、语法分析和语义分析,检查代码的正确性,并生成中间代码表示。
  • 汇编:汇编阶段将汇编代码转换为机器可以执行的目标文件。汇编器会将汇编代码转化为机器指令,并生成与机器硬件平台相关的目标文件(通常以".obj"或".o"为扩展名)。
  • 链接:链接阶段将目标文件与其他必要的库文件链接在一起,生成可执行程序。链接器会解析目标文件中的符号引用,将其与其他目标文件或库文件中的符号定义进行匹配,最终生成一个完整的可执行文件。在链接阶段,还会进行地址重定位、符号解析、符号表生成等操作,确保程序的正确执行。

静态链接库和动态链接库有什么区别?

  • 链接方式:静态链接库在编译链接时会被完整地复制到可执行文件中,成为可执行文件的一部分;而动态链接库在编译链接时只会在可执行文件中包含对库的引用,实际的库文件在运行时由操作系统动态加载。
  • 文件大小:静态链接库会使得可执行文件的大小增加,因为库的代码被完整地复制到可执行文件中;而动态链接库不会增加可执行文件的大小,因为库的代码在运行时才会被加载。
  • 内存占用:静态链接库在运行时会被完整地加载到内存中,占用固定的内存空间;而动态链接库在运行时才会被加载,可以在多个进程之间共享,减少内存占用。
  • 可扩展性:动态链接库的可扩展性更好,可以在不修改可执行文件的情况下替换或添加新的库文件,而静态链接库需要重新编译链接。

三、C++面向对象

什么是面向对象?面向对象的三大特性

对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据和方法。

  • 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
  • 继承:子类继承父类的特征和行为,子类有父类的非 private方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
  • 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向派生类的对象,使得基类指针呈现不同的表现方式。

如何理解 C++ 是面向对象编程?

以创建和使用对象为中心。一个对象(Object)就是一个软件实体,它将数据和程序在一个单元中组合起来。对象的数据项,也称为其属性,存储在成员变量中。对象执行的过程被称为其成员函数。将对象的数据和过程绑定在一起则被称为封装。

重载、重写、的区别是什么?

重载:同一作用域下,同名不同参
重写:父子类中,同名同参同返回 virtual关键词
隐藏:是指派生类的函数屏蔽了与其同名的基类函数,只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

C++的多态是什么?怎么通过虚函数实现?

多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向派生类的对象,使得基类指针呈现不同的表现方式。

  • 编译时多态(静态多态):通过函数重载和运算符重载实现,在编译时确定调用哪个函数。
  • 运行时多态(动态多态):通过虚函数实现,在运行时根据对象的实际类型决定调用的函数。实现原理是,每个包含虚函数的类都有一个虚函数表,这个表记录了该类对象可以调用的虚函数指针,每个对象也有一个隐式的虚函数指针,指向其类的虚函数表。在运行时,调用虚函数时,程序通过虚函数指针查找正确的函数。
  • 虚函数是通过在基类中声明一个函数为virtual来实现的。这标志着这个函数可以被派生类重写(override)。当通过基类指针调用虚函数时,C++会查找实际对象的类型,调用对应的子类实现,而不是基类的实现。

静态多态和动态多态的原理是什么?

静态多态和动态多态是 C++ 实现多态的两种方式,核心区别在于函数调用的绑定时机:静态多态是编译期绑定,动态多态是运行期绑定,底层实现机制和适用场景也完全不同。

静态多态(编译期多态)

1.定义:静态多态也叫编译期多态,指函数调用在编译阶段就确定好具体调用哪个版本,编译器直接完成绑定,运行时无额外开销。主要通过函数重载和模板(函数模板 / 类模板) 实现。
2. 底层原理:
针对函数重载:“编译器会通过为每个重载函数生成唯一的修饰名(比如结合函数名 + 参数类型),编译时根据实参的类型 / 个数,直接绑定到对应的修饰名,运行时无需任何判断。”
针对模板:“模板本质是「代码生成器」,编译器在编译期根据传入的模板参数(比如 int/double),实例化出具体类型的函数 / 类,最终调用的是实例化后的确定函数,同样是编译期完成绑定。”
3.优缺点:
“静态多态的优势是效率高(无运行时开销),缺点是灵活性低(类型必须编译期确定);适合类型固定、追求性能的场景,比如工具类函数重载、泛型算法(如 STL 的 sort)。”

动态多态(运行期多态)

1.定义:动态多态也叫运行期多态,指函数调用在程序运行时才确定具体版本,通过「虚函数(virtual 关键字)」实现,只有通过父类指针 / 引用调用虚函数时才会触发。
2.底层原理:
底层核心是「虚函数表(vtable)」和「虚函数表指针(vptr)」: ① 编译器会给包含虚函数的类生成一张虚函数表,表中存储该类所有虚函数的地址;
② 每个该类的对象会自动添加一个 vptr 指针(对象首个成员),指向所属类的虚函数表;
③ 子类继承父类时,会拷贝父类的虚函数表,若子类重写了虚函数,就替换表中对应函数的地址;
④ 运行时,通过父类指针 / 引用调用虚函数时,会先通过对象的 vptr 找到其实际类型的虚函数表,再从表中取出函数地址执行 —— 最终调用哪个函数,由对象的实际类型决定,而非指针 / 引用的类型。”
3. 优缺点
“动态多态的优势是灵活性高(符合开闭原则,新增子类无需修改父类代码),缺点是有少量运行时开销(查表);适合类型动态变化的场景,比如面向抽象编程(如 Animal 基类,Cat/Dog 子类)、框架设计。”

总结

① 绑定时机:静态多态编译期绑定,动态多态运行期绑定;
② 实现机制:静态多态靠名字修饰 / 模板实例化,动态多态靠虚函数表 + 虚函数表指针;
③ 工程取舍:静态多态换性能,动态多态换灵活性;实际开发中会结合使用,比如 STL 用静态多态保证性能,业务层用动态多态保证扩展性。

四、C++类相关

class中缺省的函数是什么?

在C++中,如果一个类没有显式地定义「构造函数、析构函数、拷贝构造函数、赋值运算符重载函数」,那么编译器会自动生成这些函数,这些函数被称为缺省函数。

什么是纯虚函数?有哪些应用场景

在基类中声明的虚函数=0,且没有定义,含有纯虚函数的类被称为抽象类,只能作为接口使用,但在派生类中要进行实现,如果不实现将变为纯虚函数。
主要用于:
接口设计:
设计模式:模板方法模式中,基类定义算法框架,将实现放在子类中

虚函数和纯虚函数的区别?

  1. 虚函数和纯虚函数可以出现在同一个类中
  2. 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
  3. 定义形式不同:虚函数在定义时加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;
  4. 虚函数必须实现,否则编译器会报错;
  5. 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。

虚函数的实现机制是什么?

虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数

C++空类的大小是多少?

1字节

只含虚函数的类的大小是多大?

因为有虚函数的类对象中都有一个虚函数表指针 __vptr,其大小是4字节(32位机器上)

一个只包含int 变量的空class和只包含int变量的空struct的内存各占多大?

只含有一个int成员变量的类的大小是4
在这里插入图片描述

静态成员存放在静态存储区,不占用类的大小, 普通函数也不占用类大小
在这里插入图片描述

静态成员a不占用类的大小,所以类的大小就是b变量的大小 即4个字节
在这里插入图片描述

五、C++STL

C++ STL(标准模板库)提供了多种容器,用于存储和管理数据,这些容器可分为序列式容器、关联式容器和容器适配器三大类,各自适用于不同场景:

在这里插入图片描述
序列式容器:按元素插入顺序存储,元素位置与插入顺序相关,不自动排序。

  • vector:动态数组,内存连续,支持随机访问([] 或 at()) 优点:尾插 / 尾删效率高(O(1)),适合频繁访问元素的场景。缺点:中间插入 / 删除效率低(O (n)),扩容时可能重新分配内存。
  • deque:双端队列,内存分段连续,支持首尾高效操作。优点头插 / 头删、尾插 / 尾删效率均为 O(1),可随机访问,适合场景需要在两端频繁操作的场景(如实现队列、栈)。
  • list:双向链表,元素通过指针连接,内存不连续。优点任意位置插入 / 删除效率高(O (1),只需修改指针)。缺点不支持随机访问(访问元素需遍历,O (n)),内存开销较大。
  • array:固定大小数组(C++11 新增),编译时确定大小,内存连续,比原生数组更安全(支持边界检查),但大小不可变。

关联式容器:元素按键(key) 排序存储,支持快速查找(通常 O (log n)),分为有序和无序两类。

  • set:存储唯一键值,元素自动按键升序排序,支持快速查找、插入和删除 O (log n)。适合需要去重且有序的数据集合(如存储不重复的 ID并排序)。
  • multiset:但允许键值重复,其他特性相同。
  • map:存储键值对(key-value),键唯一且自动排序,通过键快速查找值,支持快速查找、插入和删除 O (log n)。适合场景键值映射场景(如字典、配置表)
  • multimap:与 map 类似,但允许键重复(一个键可对应多个值)。

无序关联容器(C++11 新增,基于哈希表实现)

  • unordered_set / unordered_multiset:功能同 set /
    multiset,但元素无序,通过哈希表实现,查找、插入、删除平均效率更高(O (1))。适合场景对顺序无要求,但需要快速增删查的场景。
  • unordered_map / unordered_multimap:功能同 map /
    multimap,无序,基于哈希表,平均操作效率 O (1)。

容器适配器:基于其他容器实现,封装特定接口,提供受限功能。

  • stack:栈,遵循 “后进先出(LIFO)”,仅支持在顶部插入 / 删除 / 访问元素,默认基于 deque 实现,也可指定vector 或 list 作为底层容器。
  • queue:队列,遵循 “先进先出(FIFO)”,仅支持在尾部插入、头部删除。默认基于 deque 实现,也可指定 list作为底层容器。
  • priority_queue:优先队列,元素按优先级自动排序(默认最大元素在顶部),插入 / 删除效率 O (log n)。默认基于 vector 实现,底层用堆结构维护优先级。

vector中push_back和emplace_back的区别?

  • push_back()向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);
  • 而emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

C++ 的map是线程安全的么?

不是线程安全的。如果多个线程同时对 std::map 进行操作(尤其是写操作,如插入、删除、修改元素),可能会导致未定义行为(例如数据损坏、迭代器失效、程序崩溃等)。

具体来说,线程不安全主要体现在:

  • 并发写操作:多个线程同时执行插入(insert)、删除(erase)或修改(如 operator[] 赋值)时,会破坏 std::map 内部的红黑树结构(有序关联容器的底层实现),导致数据错乱。
  • 读写并发:即使一个线程读、另一个线程写,也可能出现问题。例如,读线程正在遍历 map时,写线程修改了结构,可能导致读线程的迭代器失效,触发不可预知的错误。
  • 无内置同步机制:std::map 没有提供任何锁(如互斥量)或原子操作来保证多线程安全,所有同步逻辑需要开发者手动实现。

如果要多线程安全使用map,需通过外部同步机制保证线程安全,常见方式:

  • 加锁保护:使用 std::mutex 或 std::lock_guard 等同步工具,确保同一时间只有一个线程能访问或修改 map

unordered_map的底层结构是什么?

底层结构基于哈希表实现。在这里插入图片描述

  • std::unordered_map内部维护了一个哈希桶数组,数组中的每个元素称为一个哈希桶。每个哈希桶可以存储一个或多个键值对。当多个键通过哈希函数计算得到相同的索引时,就会发生哈希冲突。
  • std::unordered_map通常使用链地址法来解决哈希冲突。在链地址法中,每个哈希桶是一个链表,当发生哈希冲突时,新的键值对会被插入到对应的链表或容器中。

C++容器可以一边遍历一边插入吗?

当处理vector,string,deque时,当在一个循环中可能增加或移除元素时,要考虑到迭代器可能会失效的问题。

vector插入的时候:

  • 当插入(push_back)一个元素后,end操作返回的迭代器肯定失效;
  • 当插入(push_back)一个元素后,如果vector的capacity发生了改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效;

list插入的时候:

  • 插入操作(insert)和接合操作(splice)不会造成原有的list迭代器失效

deque插入的时候:

  • 在deque容器首部或者尾部插入元素,不会使得任何迭代器失效;
  • 在deque容器的任何其他位置进行插入或删除操作都将使指向该容器元素的所有迭代器失效;

set和map插入的时候:

  • 与list相同,当对其进行insert或者erase操作时,操作之前的所有迭代器,在操作完成之后都依然有效,但被删除元素的迭代器失效

使用迭代器怎么删除一个元素?

  • 顺序容器(序列式容器,比如vector、deque)删除元素的方式:erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器:It= c.erase(it);
  • 关联容器(关联式容器,比如map、set、multimap、multiset等) 删除元素的方式:erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;c.erase(it++)
    “it的元素被删除,但是返回的是it++是有效的。返回的这个迭代器继续可以使用”

stl迭代器的失效情况你知道哪些?

  • 当在 vector 中插入元素时,如果插入操作导致容器的内存重新分配(即插入后容器的容量不足,需要重新分配更大的内存空间),那么所有指向vector 的迭代器、指针和引用都会失效。因为重新分配内存后,元素会被移动到新的内存位置
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3};
    auto it = vec.begin();
    vec.push_back(4); // 可能导致内存重新分配
    // 此时 it 可能已经失效
    // std::cout << *it << std::endl; // 未定义行为
    return 0;
}
  • 当在 vector 中插入元素时,如果插入操作导致容器的内存重新分配(即插入后容器的容量不足,需要重新分配更大的内存空间),那么所有指向vector 的迭代器、指针和引用都会失效。因为重新分配内存后,元素会被移动到新的内存位置,例子代码:
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3};
    auto it = vec.begin() + 1;
    vec.erase(vec.begin()); // 删除第一个元素
    // 此时 it 失效
    // std::cout << *it << std::endl; // 未定义行为
    return 0;
}
  • 在 deque 的中间插入元素时,所有迭代器、指针和引用都会失效,还有在 deque的头部或尾部插入元素时,指向元素的迭代器、指针和引用不会失效,但如果插入操作导致内存重新分配,那么迭代器可能会失效。
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3};
    auto it = deq.begin() + 1;
    deq.insert(deq.begin() + 1, 4); // 在中间插入元素
    // 此时 it 失效
    // std::cout << *it << std::endl; // 未定义行为
    return 0;
}
  • 删除 deque 中间的元素时,所有迭代器、指针和引用都会失效,还有删除 deque 头部或尾部的元素时,指向被删除元素的迭代器、指针和引用会失效。
#include <iostream>
#include <deque>

int main() {
    std::deque<int> deq = {1, 2, 3};
    auto it = deq.begin() + 1;
    deq.erase(deq.begin() + 1); // 删除中间元素
    // 此时 it 失效
    // std::cout << *it << std::endl; // 未定义行为
    return 0;
}

关联式容器(set、map),迭代器失效的情况

  • set在插入元素不会使任何迭代器、指针和引用失效,因为关联式容器使用红黑树等平衡二叉搜索树实现,插入操作只是在树中添加新节点,不会影响其他节点的内存位置。不过指向被删除元素的迭代器、指针和引用会失效,其他迭代器、指针和引用不会失效。
#include <iostream>
#include <set>

int main() {
    std::set<int> s = {1, 2, 3};
    auto it = s.begin();
    auto it_to_delete = s.find(2);
    s.erase(it_to_delete);
    // 此时 it_to_delete 失效
    // std::cout << *it_to_delete << std::endl; // 未定义行为
    std::cout << *it << std::endl; // 正常输出
    return 0;
}
  • map 在插入元素不会使任何迭代器、指针和引用失效,原因与 set 。但是指向被删除元素的迭代器、指针和引用会失效,其他迭代器、指针和引用不会失效。
#include <iostream>
#include <map>

int main() {
    std::map<int, int> m = {{1, 10}, {2, 20}, {3, 30}};
    auto it = m.begin();
    auto it_to_delete = m.find(2);
    m.erase(it_to_delete);
    // 此时 it_to_delete 失效
    // std::cout << it_to_delete->second << std::endl; // 未定义行为
    std::cout << it->second << std::endl; // 正常输出
    return 0;
}

链表式容器(list),迭代器失效

  • 在 list插入元素不会使任何迭代器、指针和引用失效,因为链表的插入操作只是修改节点的指针,不会影响其他节点的内存位置。但是,指向被删除元素的迭代器、指针和引用会失效,其他迭代器、指针和引用不会失效。
#include <iostream>
#include <list>

int main() {
    std
::list<int> lst = {1, 2, 3};
    auto it = lst.begin();
    auto it_to_delete = ++lst.begin();
    lst
.erase(it_to_delete);
    // 此时 it_to_delete 失效
    // std::cout << *it_to_delete << std::endl; // 未定义行为
    std
::cout << *it << std::endl; // 正常输出
    return 0;
}

Std::sort的底层是怎么实现的?

在这里插入图片描述
在这里插入图片描述

  1. std::sort 采用的是分治思维,先采用快速排序,将整个区域分成多个子区域,每个子区域内部根据数据量采用不同算法。
  2. 分治后,各个子区域局部有序后再通过整个区域进行排序。

四、智能指针

  1. std::unique_ptr:std::unique_ptr是一种独占所有权的智能指针,只能有一个std::unique_ptr指向同一个对象或数组。当std::unique_ptr超出作用域或被显式释放时,它会自动删除所管理的对象或数组。它通常用于表示独占的资源所有权,如动态分配的单个对象或数组。
  2. std::shared_ptr:std::shared_ptr是一种共享所有权的智能指针。可以多个std::shared_ptr指向同一个对象,通过引用计数来管理资源的生命周期。只有当最后一个std::shared_ptr超出作用域或被显式释放时,资源才会被删除。
  3. std::weak_ptr:std::weak_ptr是一种弱引用的智能指针。可以指向由std::shared_ptr管理的对象,但不会增加引用计数。std::weak_ptr主要用于解决std::shared_ptr的循环引用问题,通过std::weak_ptr.lock()方法可以获取一个有效的std::shared_ptr来访问被管理的对象。

在哪些场景下会应用智能指针?

在动态内存管理中,使用智能指针可以避免手动管理内存的麻烦和出错风险。

weak_ptr的作用是什么?如何和shared_ptr结合使用?

std::weak_ptr 主要用于辅助 std::shared_ptr 进行内存管理,解std::shared_ptr 可能存在的循环引用问题,同时还可以用于观察 std::shared_ptr 所管理对象的生命周期。

当两个或多个 std::shared_ptr 相互引用形成循环时,会导致引用计数永远不会降为 0,从而造成内存泄漏。std::weak_ptr 不会增加所指向对象的引用计数,因此可以打破这种循环引用。

智能指针会造成内存泄漏吗?

会的,比如循环引用场景,在多个shared_ptr形成循环引用,资源将无法释放。

一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?

借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。 也可以让当前unique_ptr调用release释放对ptr控制权,然后在另一个unique_ptr获取控制权。

使用智能指针会出现什么问题?怎么解决?

智能指针可能出现的问题:循环引用
循环引用的解决方法: weak_ptr

五、 内存管理

虚拟内存介绍一下

• 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
• 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
• 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

虚拟内存用户态的地址空间怎么分配的

用户态地址空间是一块连续的虚拟地址范围(比如 32 位系统通常是 0~3GB,64 位系统范围更大),按 “从低到高” 分成几块,每块有专门用途,互不干扰。在这里插入图片描述

  • 代码段,包括二进制可执行代码;
  • 数据段,包括已初始化的静态变量和全局变量;
  • BSS 段,包括未初始化的静态变量和全局变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等
  • 栈段,包括局部变量和函数调用的上下文等,比如函数里定义的int x =5,就在栈里。栈的大小是固定的,一般是 8MB。当然系统也提供了参数,以便我们自定义大小

C++中的内存分区有哪些?

在C++中,内存主要分为以下五个区域:在这里插入图片描述

  • 栈区(Stack):由编译器自动分配释放,存放函数的参数值,局部变量等。
  • 堆区(Heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。
  • 全局区(静态区)(Static):全局变量和静态变量被分配到同一块内存中。在C++中,全局区还包含了常量区,字符串常量和其他常量也是存储在此。
  • 常量区:是全局区的一部分,存放常量,不允许修改。
  • 代码区(Text):存放函数体的二进制代码。

介绍一下内存对齐

内存对齐就是就是将数据存放在内存的某个位置,使得CPU可以更快地访问到这个数据,以空间换时间的方式来提高 cpu 访问数据的性能。
在C++中,内存对齐主要涉及到两个概念:对齐边界和填充字节。

  • 对齐边界:一般情况下,编译器会自动地将数据存放在它的自然边界上。例如,int类型的数据,它的大小为4字节,编译器会将其存放在4的倍数的地址上。这就是所谓的对齐边界。
  • 填充字节:为了满足对齐边界的要求,编译器有时候需要在数据之间填充一些字节。这些字节没有实际的意义,只是为了满足内存对齐的要求。

为什么要字节对齐?

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

动态链接库怎么装载到内存的?

通过用mmap把该库直接映射到各个进程的地址空间中,尽管每个进程都认为自己地址空间中加载了该库,但实际上该库在内存中只有一份,mmap就这样很神奇和动态链接库联动起来了。

在这里插入图片描述

C++中堆和栈的区别

申请方式:

  • 栈:由系统自动分配。
  • 堆:需要程序员自己申请,并指明大小,在C语言中通过malloc函数
  • 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
  • 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

申请大小的限制:

  • 栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,操作系统中,栈的大小是几MB,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
  • 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

生命周期:

  • 栈:栈的内存管理是自动的,变量的内存会在其作用域结束时自动释放
  • 堆:堆的内存管理需要手动进行,需要使用new关键字分配内存,并使用delete或delete[]关键字释放内存,否则会导致内存泄漏。

new是在内存上哪一块去分配的内存?

  • new所申请的内存区域在C++中称为自由存储区。很多编译器的new/delete都是以malloc/free为基础来实现的,所以通常都是借由堆实现来实现自由存储,这时候就可以说new所申请的内存区域在堆上。

如果new内存失败了会是怎么样?

会抛出std::bad_alloc异常

C++ malloc和new的区别是什么?

new和malloc区别:

  • 分配内存的位置:malloc是从堆上动态分配内存,new是从自由存储区为对象动态分配内存。自由存储区的位置取决于operator
    new的实现。自由存储区不仅可以为堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
  • 返回类型安全性:malloc内存分配成功后返回void*,然后再强制类型转换为需要的类型;new操作符分配内存成功后返回与对象类型相匹配的指针类型;因此new是符合类型安全的操作符。
  • 内存分配失败返回值:malloc内存分配失败后返回NULL。new分配内存失败则会抛异常(bac_alloc)。
  • 分配内存的大小的计算:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
  • 是否可以被重载:opeartor new /operator delete可以被重载。而malloc/free则不能重载。

delete和free的区别

  • new/delete是C++的操作符,而malloc/free是C中的函数。
  • new做两件事,一是分配内存,二是调用类的构造函数;同样,delete会调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。
  • new建立的是一个对象,而malloc分配的是一块内存;new建立的对象可以用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,用指针访问,可以在里面移动指针;new出来的指针是带有类型信息的,而malloc返回的是void指针。

delete 实现原理?delete 和 delete[] 的区别?

delete 的实现原理:

  • 首先执行该对象所属类的析构函数;
  • 通过调用 operator delete 的标准库函数来释放所占的内存空间。

delete 和 delete [] 的区别:

  • delete 用来释放单个对象所占的空间,只会调用一次析构函数;
  • delete []用来释放数组空间,会对数组中的每个成员都调用一次析构函数。

malloc 的原理?malloc 的底层实现?

malloc 的原理:

  • 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;
  • 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。

malloc 的底层实现:
brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。
• mmap 内存映射原理:

  1. 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;
  2. 调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;
  3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

malloc 1KB和1MB 有什么区别?

malloc() 源码里默认定义了一个阈值:
• 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
• 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
注意,不同的 glibc 版本定义的阈值也是不同的。

六、C++新特性

说说你了解的C++11相关特性

  1. 自动类型推断(auto):引入了auto关键字,可以根据变量初始化表达式的类型自动推断变量的类型,使得代码更具灵活性和可读性。
  2. 范围for循环:通过for (element : container)语法,允许直接遍历容器中的每个元素,简化了迭代操作,减少了代码量。
  3. 移动语义和右值引用:通过引入右值引用(&&)和移动构造函数,减少了资源管理时的不必要拷贝操作,提高了性能。
  4. 智能指针:std::shared_ptr和std::unique_ptr等智能指针类的引入,帮助管理动态分配的内存,避免内存泄漏和悬挂指针等问题。
  5. Lambda表达式:引入了匿名函数的Lambda表达式语法,能够更方便地定义和使用函数对象,减少了冗余代码。
  6. nullptr空指针:引入了nullptr关键字,用于表示空指针,替代了传统的NULL,避免了空指针常量与整数间的模糊问题。
  7. 初始化列表:通过使用花括号{}来对对象初始化,统一了初始化语法,提供了更安全、简洁的初始化方式。
  8. 默认和删除成员函数:引入了=default和=delete来指明默认构造函数、拷贝构造函数等的生成和禁止。
  9. 强类型枚举:引入了枚举类(enum class),解决了传统枚举类型带来的全局命名空间和类型安全问题。
  10. 多线程支持:引入了std::thread、std::mutex等多线程支持库,使得并发编程更加方便和安全。
  11. 泛型编程优化:引入了constexpr关键字,允许在编译时计算表达式,提高了程序的性能。

七、问题排查

linux程序崩溃怎么定位问题?

如果开启了core dump文件的转存,那么程序崩溃之后,就会生成 core dump 文件,这里面会有程序运行时的堆栈信息,然后我们可以用 gdp 工具去分析 core dump 文件。

gdb myapp core

在 GDB 中,使用 bt(backtrace)命令可以查看程序崩溃时的调用栈:

(gdb) bt

如果程序崩溃问题可以重现,也可以使用 GDB 直接调试程序。在编译程序时,使用 -g 选项添加调试信息。例如:

gcc -g -o myapp myapp.c

使用 GDB 启动程序并运行,当程序崩溃时,GDB 会停在崩溃的位置。

gdb myapp
(gdb) run

程序崩溃后,使用 bt 命令查看调用栈,找出问题所在的函数和代码行。

有些程序会在崩溃时输出错误信息到标准错误输出(stderr),那么这时候直接去看错误日志就行了。

 cat error.log

内存泄露怎么避免?

可以使用使用智能指针,C++提供了智能指针(如std::shared_ptr、std::unique_ptr等),可以自动管理动态分配的内存。智能指针利用了RAII(资源获取即初始化)的原则,在对象生命周期结束时自动释放内存,避免了显式调用delete的繁琐和遗。

也可以使用内存泄漏检测工具(如Valgrind等)来分析程序,在程序运行过程中检测内存泄漏,并及时修复。

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

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

内存泄露怎么检测?

可以用 Valgrind 工具。

首先看一段 C 程序示例,比如:

#include <stdlib.h>
int main()
{
    int *array = malloc(sizeof(int));
    return 0;
}

编译程序:gcc -g -o main main.c,编译时需要加上 -g 选项打开调试,使用 IDE 的可以用 Debug 模式编译。

使用 Valgrind 检测内存使用情况:

valgrind --tool=memcheck --leak-check=full ./main

结果:

==31416== Memcheck, a memory error detector
==31416== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31416== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31416== Command: ./main_c
==31416==
==31416==
==31416== HEAP SUMMARY:
==31416==     in use at exit: 4 bytes in 1 blocks
==31416==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==31416==
==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416==    at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416==    by 0x400537: main (main.c:5)
==31416==
==31416== LEAK SUMMARY:
==31416==    definitely lost: 4 bytes in 1 blocks
==31416==    indirectly lost: 0 bytes in 0 blocks
==31416==      possibly lost: 0 bytes in 0 blocks
==31416==    still reachable: 0 bytes in 0 blocks
==31416==         suppressed: 0 bytes in 0 blocks
==31416==
==31416== For counts of detected and suppressed errors, rerun with: -v
==31416== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

先看看输出信息中的 HEAP SUMMARY,它表示程序在堆上分配内存的情况,其中的 1 allocs 表示程序分配了 1 次内存,0 frees 表示程序释放了 0 次内存,4 bytes allocated 表示分配了 4 个字节的内存。

另外,Valgrind 也会报告程序是在哪个位置发生内存泄漏。例如,从下面的信息可以看到,程序发生了一次内存泄漏,位置是 main.c 文件的第 5 行:

==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416==    at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416==    by 0x400537: main (main.c:5)

C++代码异常core dump会生成一个栈,里面内容是什么?

栈内容就是程序崩溃前的 “现场录像”,能帮你顺着调用链找到哪里出了问题,以及当时的变量状态。主要包含这些信息:

  • 函数调用链:从崩溃的那个函数开始,一层层往上列所有调用它的函数。比如 A 调用 B,B 调用 C,C 崩溃了,栈里就会显示 C → B →A 的顺序,能看出 “崩溃是从哪个入口一步步走到这里的”。
  • 每个函数的地址:记录每个函数在内存中的具体地址(比如0x400520),结合编译时的符号表(如果没被 strip掉),就能对应到具体是哪个函数(比如main()、func())。
  • 函数的参数和局部变量:每个函数调用时传入的参数值、函数内部定义的临时变量,会按栈的顺序保存。
  • 栈指针位置:记录崩溃时栈顶的位置,告诉你当前栈用到了哪里,有没有溢出。
Gemma.cpp:简化深度学习推理的利器 gemma.cpp是一个轻量级、独立的C++推理引擎,专为Google的Gemma基础模型设计。它致力于在保持简洁和直接性的同时,提供足够的灵活性以便于实验和研究。 阅读详情

相关推荐

C++编程学习:汉化版C++2.0应用软件体验

本文还有配套的精品资源,点击获取 简介:C++2.0应用软件是一款为中文用户提供便利的C++编程学习软件,它以小内存占用和高兼容性为特色,支持多种操作系统。该软件为初学者提供了一个在Windows环境下学习C++的基础平台,强调C++语言的强大功能和面向对象特性。软件包含集成开发环境(IDE),以及可能是基于Turbo C++ 2.0的子系统,用于编写、编译和运行代码。软件...

weixin_33622085的博客 1394

Keras2cpp:连接Python与C++的深度学习桥梁

Keras2cpp:连接Python与C++的深度学习桥梁 keras2cppit's a small library for running trained Keras 2 models from a native C++ code. 项目地址:https://gitcode.com/gh_mirrors/ker/keras2cpp 在深度学习的世界里,Keras以其简洁的API设计和强大的功...

gitblog_00084的博客 655

C++权威指南:深入学习C++ 2nd Edition

C++的复杂数据类型包括结构体(struct)、联合体(union)、枚举(enum)和类(class)。它们为数据的组织和抽象提供了更丰富的结构。// 定义结构体int x;int y;// 使用结构体Point p;p.x = 5;p.y = 10;函数是组织代码的重要方式之一,它允许我们将重复使用的代码块封装起来,便于管理和复用。// 函数声明// 函数定义// 函数调用在C++中,类是创建对象的蓝图。类定义包含数据成员(属性)和成员函数(方法)。

weixin_35916518的博客 629

第2章-cpp开始学习C++

生成可执行代码的过程: #include <iostream> int main() { using namespace std; cout << "Come up and C++ me some time."; cout << endl; cout << "You won't regret it!" <...

itzyjr的专栏 582

病理WSI配准库valis教程

病理WSI配准库valis教程

azrealq的博客 2598

Foxglove 介绍以及入门使用教程

Foxglove 是由Foxglove 工作室开发的一套工具集,包括和等组件。跨平台支持:可以在 Windows、macOS 和 Linux 上运行。多协议兼容:支持 ROS、ROS 2、CAN 数据以及其他自定义协议。丰富的可视化功能:可以绘制点云、轨迹、激光雷达数据、IMU 数据等。实时与离线模式:既可以实时监控机器人状态,也可以加载和回放记录的数据文件。Foxglove 的 3D 视图允许你以三维方式查看机器人的运动轨迹、传感器数据和其他几何信息。你可以旋转、缩放和平移视图以获得更好的观察角度。

zolty 1万+

C++的学习day2.cpp

开个玩笑,我不更新其实是因为发现学校教的内容的侧重点与网络上的诸多教程相差甚远,学校更侧重的是应试,而网络上的教程更侧重于实用.不能否认,学校的教学方式确实可以帮助我们理解语言的一些特性,但那些东西还是留给课上的自己吧,所以我才不愿意在文章中分享。编程应该是有趣的.所以,我尽量分享的一些有趣的,我也尽量用轻松的语言来分享.比如输入4月,可以将1月,2月,3月的天数直接加进去,不用做多余的处理.打个比方,当mouth为4时,也就是case 4的情况,而mouth ,我的兄弟,你的值始终是4,从未改变……

2301_81053549的博客 166

实现飞书机器人推送消息到指定群组或者用户

飞书是一个集成了聊天、协作和办公功能的企业级平台,广泛应用于企业内部沟通和协作。飞书机器人(Feishu Bot)是一种强大的工具,允许开发者通过编程接口与飞书进行互动,从而实现自动化消息推送和任务管理。本文将详细介绍如何通过飞书机器人推送消息给指定的人或者群组,帮助您快速上手。

qq_41898196的博客 5865

[CPP]大一学生学习C++的笔记(2)

[CPP]C++学习之路——类的组合

maets906的博客 287

飞书多维表格省钱攻略:1个专业版账号,全员合规用、权限全掌控

像运动馆、 托管班、作业辅导、少儿美术、书法、口才班等等。对于大多数中小团队而言,飞书多维表格的核心需求是“协作高效、数据可控、成本可控”,而“1个专业版账号+多联系人”的模式,恰好完美契合这三点——既不用为不必要的席位付费,又能解锁全部专业功能,还能通过精细化权限管控保障数据安全。尤其是餐饮店、乒乓球训练馆这类个体经营或小型团队,人员分工清晰、权限需求明确,采用这种模式能最大化节省成本,同时提升管理效率。无需复杂的配置,无需额外的技术支持,5分钟就能搭建起全员协作的省钱体系。

涛涛讲AI 2215

LeRobot SO-ARM100 学习笔记(2) diffusion module

对 lerobot 中的 diffusion policy 进行基本的代码阅读

mikhailbran的博客 1586

CPP-Summit-2022 学习:当C++遇见Redis2

整体分层: 2. 快速开始 编译 最小使用示例 3. RESP2 协议基础 RESP2(REdis Serialization Protocol v2)是纯文本协议,每行以 结尾。 第一个字节决定消息类型: 命令都以 Array 发送,例如 : Bulk String 的两个特殊值: Array 的两个特殊值: 4. redis_client.hpp 逐层解读 Section 1:数据类型 为什么 Array 用 ? 和 互相引用: 包含 , 包含 。 如果直接 ,编译器在定义 时 还不完整

虾球 308

小程序强制更新避坑指南:手把手教你实现无缝升级

本文详细解析小程序强制更新的实现方法,重点介绍wx.getUpdateManager API的使用技巧,帮助开发者规避冷热启动机制下的版本管理陷阱。通过实战案例展示如何设计多级更新提示策略,确保用户无缝升级,提升小程序版本更新率。

star的博客 280

[学习笔记] 2. C++ / CPP核心编程

C++核心编程

Le0v1n 的博客 985

Matlab波动光光学系统

波动光学是一种研究光波传播过程的理论,按照其处理的信息类型不同,可以分为相干和非相干波动光学,相干波动光学可以用作特定光学系统的分析和设计,其主要依赖物理光学和电磁波的基本原理,以及光学干涉和衍射的经验方程式,其中光波传播过程是以复数形式表示的,包括振幅和相位两个方面。近年来光学技术得到了广泛的应用和发展,波动光光学系统是其中一种重要的光学系统,它主要是利用波动光学原理,通过计算机模拟实现各种光学系统的设计、分析和优化,可应用于大气光传输、光束整形、分析系统的衍射和干涉特性,包括利用。

weixin_50547796的博客 260

Kafka 入门篇(一)

初识 Kafka 什么是 Kafka Kafka 的基本术语 Kafka 的特性(设计原则) Kafka 的使用场景 Kafka 的消息队列 点对点模式的消息队列 发布-订阅模式的消息队列 Kafka 系统架构 核心 API Kafka 为何如此之快 broker 端配置 broker.id port zookeeper.connect log.dirs num.recovery.threads.per.data.dir auto.create.topics.enable topic默认配置 num.par

1232

Cpp2IL 开源项目教程

Cpp2IL 是一个强大的开源项目,旨在将 C++ 编译后的 DLL 或 EXE 文件转换为 IL(Intermediate Language)代码。这使得开发者能够更深入地理解和分析这些二进制文件。IL 是 .NET 框架中的中间语言,可以被 Just-In-Time (JIT) 编译器进一步转化为机器码执行。通过这种方式,原本对 C++ 源代码不可见的程序逻辑变得可见,对于逆向工程和代码审查非...

gitblog_00118的博客 1356

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

探索ROS2的精髓:C++和Python学习资源库

探索ROS2的精髓:C++和Python学习资源库 去发现同类优质开源项目:https://gitcode.com/ 在这个日新月异的机器人操作系统(ROS)世界中,不断学习和掌握新技术至关重要。ROS2 C++ Python learning 是一个精心策划的开源项目,旨在帮助开发者快速上手并深入理解ROS2的基础、中级以及高级特性。这个项目以一系列可以直接编译和运行的示例节点为基础,为你的RO...

gitblog_00092的博客 542

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Keras2cpp 开源项目教程

Keras2cpp 是一个开源项目,旨在将 Keras 模型转换为 C++ 代码,以便在 C++ 环境中运行深度学习模型。该项目由 Piotr Płoński 开发,主要利用 Keras 的模型序列化功能,将其转换为可在 C++ 中加载和运行的格式。 ## 项目快速启动 ### 安装依赖 首先,确保你已经安装了 Python 和 Keras。你可以通过以下命令安装 Keras: ```ba

gitblog_00400的博客 737

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

CPP-Summit-2025 学习:C++跨越40载的成功经验2

生存期控制由五种操作构成,覆盖对象从诞生到消亡的全过程: 生存期控制={构造(Construction)建立不变量,获取资源析构(Destruction)释放资源,结束生命拷贝构造(Copy Construction)从现有对象创建副本拷贝赋值(Copy Assignment)用现有对象覆盖另一个移动构造(Move Construction)转移资源所有权(创建时)移动赋值(Move Assignment)转移资源所有权(赋值时)\text{生存期控制} = \begin{cases} \text{构造(C

虾球 387

推荐开源项目:go2cpp - 高效的Go到C++转换器

推荐开源项目:go2cpp - 高效的Go到C++转换器 项目介绍 在软件开发中,寻找适合特定场景的高效编程语言至关重要。go2cpp是一个创新的开源工具,它旨在将Go语言编写的代码转换为C++,帮助开发者利用Go语言的简洁和强大,同时享受到C++的高性能和低级控制。这个工具通过分析Go编译成的WebAssembly (Wasm) 文件,生成相应的C++代码,使得跨语言的工程实施成为可能。 项目技...

gitblog_00019的博客 868

cpp学习记录2

野指针:被声明但未初始化(赋值)的指针。这个指针会指向随机的内存空间,可能导致位置问题。三类:指向const的指针,const指针,指向const的const指针。指针指向区域已经被回收(delete),这种问题称之为:指针悬挂。C++变量,数组创建是自动分配内存的,称之为静态内存分配。指针:特定类型数据在内存中的储存地址,即内存地址。表示指向区域的数据,是不变的,但可以更换指向。需要手动的管理内存,即手动分配,用完清理。表示指针本身不可更改,但指向的数据可以更改。指针和指向的区域的值,都不可改变。

JTB__JJ的博客 425

CPP 基础学习2

c++

wade1010的专栏 211

【log4cpp_学习】2_log4cpp配置文件的使用

log4cpp配置文件

田一一 1815

cpp学习笔记2

这部分主要学习STL,并且补充数据结构的知识点。图片摘自。

gagayyx的博客 421

【学习记录】vscode+ros2+cpp调试

本文仅用于个人记录。

larrydong的博客 802
上一篇: 数据库学习
下一篇: 计算机网络
Cyrus Xin
博客等级 码龄8年 7粉丝 17原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值