C++特点

1. 面向对象编程(OOP)

C++ 支持面向对象编程,提供了以下核心特性:

  • 类(Class):用于定义对象的属性和行为。

  • 封装(Encapsulation):通过访问控制(如 publicprivateprotected)隐藏实现细节。

  • 继承(Inheritance):支持代码重用和层次化设计。

  • 多态(Polymorphism):允许通过基类指针调用派生类的函数。多态是指同一个接口可以表现出不同的行为。在 C++ 中,多态主要通过 虚函数(Virtual Function)函数重载(Function Overloading) 来实现。

    • 编译时多态:通过函数重载和运算符重载实现。
    • 运行时多态:通过虚函数和继承实现。
    • 允许子类重写父类的虚函数,实现不同的行为

2. 高效性

  • C++ 继承了 C 语言的底层操作能力,可以直接操作内存和硬件。
  • 支持手动内存管理(如 newdelete),但也提供了智能指针(如 std::unique_ptrstd::shared_ptr)来简化内存管理。
  • 性能接近 C 语言,适合开发高性能应用(如游戏引擎、操作系统等)。

C语言和C++的区别

C 语言

  • 主要支持过程式编程。

  • 通过函数和结构体组织代码。

  • 标准库较小

  • 完全手动管理内存,使用 mallocfree

  • 不支持函数重载。

  • 不支持异常处理。

C++语言

  • 支持多种编程范式,包括过程式、面向对象和泛型编程。

  • 通过类、对象、模板等实现代码组织。

  • 标准库更丰富,包括标准模板库(STL)、容器、算法、迭代器等。

  • 支持手动内存管理(newdelete),但也提供了智能指针(如 std::unique_ptrstd::shared_ptr)来自动管理内存。

  • 支持函数重载,允许定义多个同名函数,只要它们的参数列表不同。

  • 支持异常处理机制(trycatchthrow)。

说说 C++中struct 和class 的区别

**struct**:

  • 默认的成员访问权限是 public
  • 通常用于表示简单的数据结构,类似于 C 语言中的结构体。
  • 适合存储一组相关的数据,而不涉及复杂的操作。
  • 默认的继承方式是 public

**class**:

  • 默认的成员访问权限是 private

  • 通常用于表示具有行为和属性的对象,强调封装和抽象。

  • 适合实现面向对象编程中的类,包含数据成员和成员函数。

  • 默认的继承方式是 private

include头文件的顺序以及双引号””和尖括号<>的区

头文件顺序

  • 先包含标准库的头文件(如 <iostream><vector> 等)。

  • 然后包含第三方库的头文件(如 <boost/any.hpp>)。

  • 最后包含用户自定义的头文件(如 "myheader.h")。

原因:标准库和第三方库的头文件通常不会依赖用户自定义的头文件,因此先包含它们可以避免潜在的依赖问题。用户自定义的头文件可能依赖标准库或第三方库,因此放在最后。

**尖括号 <>**:

  • 用于包含标准库或第三方库的头文件。
  • 编译器会在标准库路径和系统默认路径中查找这些头文件。

**双引号 ""**:

  • 用于包含用户自定义的头文件。
  • 编译器会先在当前文件所在目录中查找头文件,如果找不到,再按照尖括号的方式查找。

说说C++结构体和C结构体的区别

C 结构体

  • 只能包含数据成员,不能包含成员函数。
  • 所有成员默认是 public,没有访问控制的概念。
  • 不支持默认成员初始化。
  • 定义结构体变量时需要显式使用 struct 关键字。

C++ 结构体

  • 可以包含数据成员和成员函数。

  • 可以使用 publicprivateprotected 关键字来控制成员的访问权限。

  • 默认访问权限是 public

  • 支持继承,可以继承其他结构体或类。

  • 可以定义构造函数和析构函数。

  • 支持默认成员初始化。

  • 定义结构体变量时不需要 struct 关键字。

导入C函数的关键字是什么,C++编译时和C有什么不同?

在 C++ 中导入 C 函数的关键字是 extern "C"。它的作用是告诉 C++ 编译器按照 C 语言的命名规则(name mangling)来编译函数,以便与 C 代码兼容。

C++从代码到可执行二进制文件的过程

1. 预处理(Preprocessing)

预处理阶段由 预处理器 完成,主要任务是对源代码进行文本级别的处理,生成一个 预处理后的源代码文件

  • 宏展开:将 #define 定义的宏替换为实际值。
  • 头文件包含:将 #include 指定的头文件内容插入到源代码中。
  • 条件编译:根据 #if#ifdef#ifndef 等条件编译指令,决定是否包含某些代码块。
  • 删除注释:删除源代码中的注释。

2. 编译(Compilation)

编译阶段由 编译器 完成,将预处理后的源代码文件转换为 汇编代码(通常以 .s 为扩展名)。

  • 词法分析:将源代码分解为有意义的词法单元(如关键字、标识符、运算符等)。
  • 语法分析:根据语法规则检查代码的结构是否正确,生成语法树。
  • 语义分析:检查代码的语义是否正确(如类型检查)。
  • 代码优化:对代码进行优化,以提高执行效率。
  • 生成汇编代码:将高级语言代码转换为目标机器的汇编代码。

3. 汇编(Assembly)

汇编阶段由 汇编器 完成,将汇编代码转换为 目标文件(通常以 .o.obj 为扩展名)。目标文件是机器代码的二进制表示,但尚未链接为最终的可执行文件。

4. 链接(Linking)

链接阶段由 链接器 完成,将多个目标文件和库文件合并为一个 可执行文件(如 .exe.out)。

static关键字的作用

1. 在函数内部(局部变量)

static 用于函数内部的局部变量时,它会改变变量的存储周期和作用域:

  • 存储周期:变量在程序的生命周期内一直存在,而不是在函数调用结束后销毁。
  • 作用域:变量仍然只能在函数内部访问。
  • 初始化:只初始化一次,后续调用函数时,变量会保留上一次的值。

2. 在类中(静态成员变量)

static 用于类的成员变量时,它会使其成为 类级别的变量,而不是实例级别的变量:

  • 共享性:所有类的实例共享同一个静态成员变量。
  • 存储周期:静态成员变量在程序的生命周期内存在。
  • 访问方式:可以通过类名直接访问,而不需要创建类的实例。

3. 在类中(静态成员函数)

static 用于类的成员函数时,它会使其成为 类级别的函数

  • 访问方式:可以通过类名直接调用,而不需要创建类的实例。
  • 限制:静态成员函数只能访问静态成员变量和静态成员函数,不能访问非静态成员(因为它们没有 this 指针)。

什么是函数指针,如何定义函数指针,有什么使用场景

函数指针是指向函数的指针变量。它存储了函数的地址,可以通过函数指针调用该函数。

如何定义函数指针?

函数指针的定义包括以下部分:

  1. 函数返回类型:指定函数返回值的类型。
  2. 指针名称:为函数指针命名。
  3. 参数列表:指定函数的参数类型和数量。

语法:

1
返回类型 (*指针名称)(参数类型1, 参数类型2, ...);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int main() {
// 定义函数指针
int (*funcPtr)(int, int);

// 将函数地址赋值给函数指针
funcPtr = add;
std::cout << "Add: " << funcPtr(10, 5) << std::endl; // 输出: 15

funcPtr = subtract;
std::cout << "Subtract: " << funcPtr(10, 5) << std::endl; // 输出: 5

return 0;
}

1. 回调函数(Callback Functions)

回调函数是通过函数指针传递给其他函数的函数。当某个事件发生时,回调函数会被调用。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void process(int x, int y, int (*callback)(int, int)) {
int result = callback(x, y);
std::cout << "Result: " << result << std::endl;
}

int add(int a, int b) {
return a + b;
}

int main() {
process(10, 5, add); // 输出: Result: 15
return 0;
}

2. 动态选择函数

通过函数指针,可以在运行时根据条件选择要调用的函数。

静态变量什么时候初始化?

1. 局部静态变量(函数内部的静态变量)

  • 初始化时机:在第一次执行到该变量的定义语句时初始化。
  • 特点:
    • 只初始化一次,即使函数被多次调用,静态变量也不会重新初始化。
    • 如果初始化过程中抛出异常,后续尝试访问该变量会导致未定义行为。

2. 全局静态变量(文件作用域的静态变量)

  • 初始化时机:在程序启动时(main 函数执行之前)进行初始化。
  • 特点:
    • 初始化的顺序与变量在文件中的定义顺序一致。
    • 如果初始化过程中抛出异常,程序会终止。

3. 类的静态成员变量

  • 初始化时机:在程序启动时(main 函数执行之前)进行初始化。
  • 特点:
    • 必须在类外显式定义和初始化。
    • 初始化的顺序与变量在文件中的定义顺序一致。
    • 如果初始化过程中抛出异常,程序会终止。

4. 命名空间中的静态变量

  • 初始化时机:在程序启动时(main 函数执行之前)进行初始化。
  • 特点:
    • 初始化的顺序与变量在文件中的定义顺序一致。
    • 如果初始化过程中抛出异常,程序会终止。

5. 静态变量的销毁时机

静态变量的销毁时机与初始化时机相对应:

  • 局部静态变量:在程序结束时销毁。
  • 全局静态变量、类的静态成员变量、命名空间中的静态变量:在程序结束时销毁。
  • 销毁顺序:与初始化顺序相反。

nullptr调用成员函数可以吗?为什么?

在 C++ 中,使用 nullptr 调用成员函数是未定义行为(Undefined Behavior),但具体表现取决于是否访问了对象的成员数据。

在 C++ 中,成员函数的调用需要通过对象(或指针)来进行。当使用 nullptr 调用成员函数时,实际上是通过一个空指针访问对象,这会导致未定义行为。

未定义行为意味着:

  • 程序可能会崩溃(如段错误)。
  • 程序可能会继续运行,但产生错误的结果。
  • 程序的行为可能因编译器、平台或运行环境的不同而不同。

如果成员函数没有访问对象的成员数据,且编译器没有对空指针进行检查,程序可能不会崩溃。这是因为:

  • 成员函数在编译时会被转换为普通函数,第一个参数是对象的地址(this 指针)。
  • 如果函数中没有使用 this 指针(即没有访问成员数据),程序可能不会立即崩溃。

如果成员函数访问了对象的成员数据,程序会尝试通过 this 指针访问无效的内存地址,从而导致崩溃。

说说什么是野指针,怎么产生的,如何避免

野指针(Dangling Pointer)是指指向已经释放或无效内存的指针。访问野指针会导致未定义行为(Undefined Behavior),可能导致程序崩溃、数据损坏或安全漏洞。

野指针的产生原因

野指针通常由以下几种情况引起:

释放内存后未置空指针

  • 当动态分配的内存被释放(如使用 deletefree)后,指针仍然指向原来的内存地址,但该地址已经无效。

函数返回局部变量的地址

  • 函数返回局部变量的地址后,局部变量在函数结束时被销毁,指针指向的内存无效。

对象生命周期结束

  • 当对象的生命周期结束时,指向该对象的指针变为野指针。

如何避免野指针的产生

释放内存后置空指针

  • 在释放内存后,将指针置为 nullptr,避免重复释放或访问无效内存。

避免返回局部变量的地址

  • 不要返回局部变量的地址,而是返回动态分配的内存或静态/全局变量的地址。

使用智能指针

  • 使用 std::unique_ptrstd::shared_ptr 等智能指针,自动管理内存的生命周期,避免野指针。

说说静态局部变量,全局变量,局部变量的特点,以及使用场景

在 C++ 中,变量根据其作用域和生命周期可以分为局部变量静态局部变量全局变量。它们各自有不同的特点和适用场景。以下是详细的对比分析:


1. 局部变量

特点:

  • 作用域:仅限于定义它的代码块(如函数、循环、条件语句等)。
  • 生命周期:从定义处开始,到代码块结束时销毁。
  • 存储位置:通常存储在栈(Stack)中。
  • 初始化:如果未显式初始化,其值是未定义的(垃圾值)。
  • 访问性:只能在定义它的代码块内访问。

使用场景:

  • 用于函数内部的临时计算或存储。
  • 适用于生命周期短、作用域小的场景。

2. 静态局部变量

特点:

  • 作用域:仅限于定义它的代码块(如函数)。
  • 生命周期:从第一次定义开始,直到程序结束。
  • 存储位置:存储在全局/静态存储区。
  • 初始化:如果未显式初始化,其值会被默认初始化为 0
  • 访问性:只能在定义它的代码块内访问,但其值在多次函数调用之间保持不变。

使用场景:

  • 用于在函数调用之间保持状态。
  • 适用于需要持久化局部数据的场景。

3. 全局变量

特点:

  • 作用域:整个程序(所有文件或通过 extern 声明访问)。
  • 生命周期:从程序开始到程序结束。
  • 存储位置:存储在全局/静态存储区。
  • 初始化:如果未显式初始化,其值会被默认初始化为 0
  • 访问性:可以在程序的任何地方访问。

使用场景:

  • 用于在整个程序中共享数据。
  • 适用于需要跨多个函数或文件访问的数据。

说说内联函数和宏函数的区别

堆和栈的区别:

栈(Stack) 在计算机科学中也称为 堆栈,是一种遵循 先进后出(FILO,First In Last Out)后进先出(LIFO,Last In First Out) 原则的数据结构。

堆是一种动态内存分配区域,主要用于存储程序运行时动态分配的数据。堆的管理方式与栈完全不同:

  • 堆的内存分配是随机的,不遵循任何特定的顺序。
  • 堆的大小不固定,由程序员手动管理。

例如:

1
2
3
4
5
6
7
8
Cint* ptr1 = (int*)malloc(sizeof(int)); // 在堆上分配内存
*ptr1 = 10;

int* ptr2 = (int*)malloc(sizeof(int)); // 在堆上分配另一块内存
*ptr2 = 20;

free(ptr1); // 释放第一块内存
free(ptr2); // 释放第二块内存

malloc和局部变量分配在堆还是栈?

1. malloc 分配的内存

malloc 函数用于在 堆(Heap) 上动态分配内存。

特点

  • 堆是一块由程序员手动管理的内存区域。
  • 分配的内存需要显式释放(使用 free 函数),否则会导致内存泄漏。
  • 堆的大小受系统内存限制,通常比栈大得多。
  • 堆内存的生命周期由程序员控制,分配的内存在 free 之前一直有效。

2. 局部变量分配的内存

局部变量分配在 栈(Stack) 上。

特点

  • 栈是由编译器自动管理的内存区域。
  • 栈内存的生命周期与函数调用相关,函数返回时自动释放。
  • 栈的大小有限(通常几 MB),分配过大的局部变量可能导致栈溢出。
  • 栈的分配和释放速度比堆快。

程序有哪些section,分别的作用?程序启动的过程?

程序的 Section(段)及其作用

在编译后的可执行文件中,程序通常被划分为多个段(Section),每个段有不同的作用。以下是常见的段及其功能:

1. .text 段

  • 作用:存放程序的 代码(指令),即编译后的机器指令。
  • 特点:
    • 通常是只读的,防止程序运行时意外修改代码。
    • 包含函数、主程序等的指令。

2. .data 段

  • 作用:存放 已初始化 的全局变量和静态变量,注意存储已初始化且初始值非 0 的全局变量和静态变量。。
  • 特点:
    • 这些变量在程序启动时就已经被赋值。
    • 可读可写。

3. .bss 段

  • 作用:存放 未初始化 的全局变量和静态变量。
  • 特点:
    • 在程序加载时,这些变量会被初始化为 0。
    • 不占用磁盘空间,仅记录变量的大小。

4. .rodata 段

  • 作用:存放 只读数据,如字符串常量、常量数组等。
  • 特点:
    • 通常是只读的,防止程序运行时修改。

5. .heap 段

  • 作用:用于 动态内存分配(如 mallocnew)。
  • 特点:
    • 由程序员手动管理。
    • 大小不固定,随着程序运行动态增长。

6. .stack 段

  • 作用:用于 函数调用,存放局部变量、函数参数、返回地址等。
  • 特点:
    • 由编译器自动管理。
    • 大小有限,通常在几 MB 左右。

7. 其他段

  • .plt/.got:用于动态链接,存放过程链接表(PLT)和全局偏移表(GOT)。
  • .init/.fini:存放程序的初始化和终止代码。

程序启动的过程

程序从加载到运行的整个过程可以分为以下几个阶段:

1. 加载(Loading)

  • 作用:将可执行文件从磁盘加载到内存。
  • 过程:
    • 操作系统读取可执行文件的头部信息,确定需要加载哪些段。
    • .text.data.rodata 等段加载到内存的指定位置。
    • .bss 段分配内存并初始化为 0。

2. 初始化(Initialization)

  • 作用:初始化程序的运行环境。
  • 过程:
    • 设置堆(heap)和栈(stack)的起始地址。
    • 调用 .init 段中的初始化代码(如果有)。
    • 初始化全局变量和静态变量。

3. 执行(Execution)

  • 作用:开始执行程序的主逻辑。
  • 过程:
    • main 函数开始执行。
    • 程序运行时,栈用于函数调用,堆用于动态内存分配。

4. 终止(Termination)

  • 作用:程序正常结束或异常终止。
  • 过程:
    • 调用 .fini 段中的终止代码(如果有)。
    • 释放堆内存。
    • 返回退出状态给操作系统。

内存对齐的主要使用场景包括:

  1. 提高内存访问效率。
  2. 满足硬件要求。
  3. 避免缓存行冲突。
  4. 与硬件设备交互。
  5. 优化数据结构。
  6. 确保跨平台兼容性。

内存泄漏

内存泄漏(Memory Leak) 是指程序在运行过程中动态分配的内存未能正确释放,导致内存占用不断增加,最终可能耗尽系统内存资源。以下是内存泄漏的原因、检测方法和解决方案:

1. 内存泄漏的原因

  • 忘记释放内存:

    • 使用 newmalloc 分配内存后,未调用 deletefree 释放。
  • 指针丢失

    • 指针被重新赋值或超出作用域,导致无法访问已分配的内存。
  • 异常导致未释放

    • 在释放内存之前发生异常,导致内存未释放。
  • 循环引用

    • 在智能指针或对象之间存在循环引用,导致引用计数无法归零。

2. 检测内存泄漏的方法

  • 手动检查:

    • 仔细检查代码,确保每次 newmalloc 都有对应的 deletefree
  • 使用工具:

    • Valgrind(Linux):检测内存泄漏和非法内存访问。
  • 日志记录

    • 在分配和释放内存时记录日志,方便追踪。

3. 解决内存泄漏的方案

  • 使用智能指针:

    • 使用 std::unique_ptrstd::shared_ptr 自动管理内存。
  • RAII(资源获取即初始化)

    • 将资源(如内存)的生命周期与对象的生命周期绑定。
  • 避免裸指针

    • 尽量避免直接使用裸指针,改用容器(如 std::vectorstd::string)。
  • 检查循环引用

    • 使用 std::weak_ptr 打破循环引用。
  • 异常安全

    • 使用 RAII 或智能指针确保异常发生时资源仍能释放。