1.1.1 简述C++语言的特点
- C++在C语言基础上引入面向对象的机制,同时也兼容C语言。
- C++有三大特性:封装:通过类和对象将数据和操作封装在一起,隐藏实现细节。继承:子类可以继承父类的属性和行为,支持代码复用和扩展性。多态:支持静态多态(函数重载、运算符重载)和动态多态(虚函数),使得程序更加灵活。
- 高效性:C++是一种编译型语言,程序在执行前被编译成机器码,执行效率高,接近底层硬件。使其适用于对性能要求较高的应用场景,如操作系统,游戏引擎和嵌入式系统。仅比汇编语言慢10-20%。
- 内存管理灵活:支持手动内存管理,也可以使用智能指针进行自动化内存管理。智能指针如std::shared_ptr和std::unique_ptr可以自动管理内存,避免常见的内存泄漏问题。
- 跨平台支持 Windows、Linux、macOS 等平台
- 可复用性高,引入模板的概念,并开发了标准库STL,提供了常用的数据结构和算法(如vector、map\sort),简化了复杂程序的实现。
- 异常处理和多线程支持。
1.1.2 C语言和C++的区别
- C语言是面向过程的编程语言,C++是面向对象的编程语言,C++在C的基础上一如很多新特性,如引用、智能指针、auto变量等。
- 面向对象:C不支持类和对象,C++支持封装、继承、多态等面向对象特性。
- 模板与泛型编程: C不支持模板,通用代码通过 void* 实现。C++引入了模板,允许泛型编程,实现类型无关的通用代码。
- 内存管理:C使用malloc/free进行手动内存管理,C++有new/delete和智能指针简化内存管理。
- 函数与运算符重载:C不支持函数和运算符重载,C++允许重载,提高代码灵活性和可读性。
- 标准库:C的标准库简单,C++有标准模板库(STL),提供丰富的数据结构和算法集合。
1.1.3 C++中struct和class的区别
以下是包含C语言中的struct
与C++中的struct
和class
区别的完整表格:
特性 | C语言中的struct | C++中的struct | C++中的class |
---|---|---|---|
默认成员访问权限 | 无(所有成员默认都是公开的) | public | private |
继承的默认访问权限 | 不支持继承 | public | private |
面向对象支持 | 不支持 | 支持(可以包含构造函数、析构函数、继承、多态等) | 支持(可以包含构造函数、析构函数、继承、多态等) |
用途 | 用于简单的数据结构,主要存储数据 | 通常用于简单的数据结构,主要存储数据 | 通常用于复杂对象,包含数据和方法,强调封装 |
语法差异 | 无成员函数,不能包含方法或构造函数 | 与class 相同,除了默认访问权限 | 与struct 相同,除了默认访问权限 |
在C++中,struct
和class
的主要区别在于默认的访问权限,除此之外,它们在其他方面非常相似。具体来说:
1. 默认访问权限
- struct:默认情况下,
struct
的成员(变量和函数)是public的。 - class:默认情况下,
class
的成员是private的。
2. 继承的默认访问权限
- struct:使用
struct
时,继承的默认访问权限是public。 - class:使用
class
时,继承的默认访问权限是private。
3. 语法上的一致性
- 除了默认访问权限的区别,
struct
和class
几乎完全相同。它们都可以包含成员变量、成员函数、构造函数、析构函数、以及支持继承和多态。 - 也就是说,你可以在
struct
中实现面向对象的特性,就像在class
中一样。
示例
// struct 默认成员是 public
struct MyStruct {int x; // publicMyStruct() {} // public 构造函数
};// class 默认成员是 private
class MyClass {int y; // private
public:MyClass() {} // public 构造函数
};
什么时候用struct
vs class
- struct:通常用于表示简单的数据结构,主要保存数据,类似于C语言中的结构体。
- class:通常用于更复杂的对象,特别是涉及封装、继承和多态等面向对象特性时。
在功能上,struct
和class
是等效的,区别主要是基于设计风格和习惯。
1.1.4 include头文件的顺序以及双引号""和尖括号<>的区别
在面试中,针对头文件的包含顺序和双引号 ""
与尖括号 <>
的区别,可以简洁地回答如下:
头文件的包含顺序:
- 先包含项目的自定义头文件,使用双引号
""
。 - 然后包含第三方库头文件,可以用
""
或<>
。 - 最后包含标准库头文件,使用尖括号
<>
。
双引号 ""
和尖括号 <>
的区别:
- 双引号
""
:优先在当前目录或用户指定目录中查找头文件,常用于自定义头文件。 - 尖括号
<>
:直接在系统目录或标准库目录中查找头文件,通常用于标准库或系统库。
1.1.5 C结构体和C++结构体的区别
“在C中,结构体只能包含数据成员,不能有函数成员或构造函数,且没有访问控制,声明时需要使用struct
关键字。C++对结构体做了扩展,它支持成员函数、构造函数、访问控制,还可以继承和多态,使用方式类似类,声明时不需要struct
关键字。”
特性 | C语言中的结构体 | C++中的结构体 |
---|---|---|
成员类型 | 只能包含数据成员 | 可以包含数据成员和函数成员 |
访问控制 | 没有访问控制,所有成员都是公开的 | 支持public 、private 、protected ,默认public |
继承和多态 | 不支持 | 支持继承和多态 |
构造函数/析构函数 | 不支持 | 支持构造函数和析构函数 |
声明变量时是否需要struct | 需要使用struct 关键字 | 不需要struct 关键字 |
1.1.6 导入C函数的关键字是什么,C++编译时和C有什么不同?
extern “C” :用于声明C语言函数,使得C++编译器以C的方式链接它们,避免C++编译器的符号修饰(name mangling)。C编译器没有符号修饰,函数名较为简单,而C++为了支持函数重载、模板等特性,对函数名进行符号修饰,因此需要extern "C"来保证与C代码的兼容。”
// 告诉C++编译器这是一个C函数
extern "C" void my_c_function(int a);
//多个C函数需要在C++中使用。
extern "C" {void my_c_function1(int a);void my_c_function2(double b);
}
“符号修饰是编译器为了支持C++中的函数重载、模板、命名空间等特性,对函数名、变量名进行编码,使得每个符号在编译生成的二进制文件中是唯一的。C++中使用符号修饰来区分同名函数或变量,而C语言不支持函数重载,因此不需要符号修饰。为了与C语言代码兼容,C++提供了extern "C"关键字来关闭符号修饰。”
1.1.7 简述C++从代码到可执行二进制文件的过程
简要面试回答:
“C++代码到可执行文件的过程包括四个阶段:预处理阶段处理宏、文件包含等;编译阶段将代码翻译为汇编代码;汇编阶段将汇编代码转为机器代码生成目标文件;链接阶段将目标文件和库文件组合,解析符号并生成最终的可执行文件。”
C++从代码到可执行二进制文件的生成过程可以分为四个主要阶段:预处理、编译、汇编和链接。每个阶段都对源代码进行不同的转换,最终生成可执行文件。
1. 预处理(Preprocessing)
在这个阶段,编译器对源代码中的预处理指令进行处理,例如:
- 宏定义:将宏展开。
- 文件包含:处理
#include
指令,将头文件的内容插入到代码中。 - 条件编译:根据
#if
、#ifdef
等条件预处理指令,决定哪些代码片段会被编译。 - 注释移除:删除代码中的注释。
预处理的结果是一个纯粹的C++源代码文件,其中已经包含了所有头文件、宏展开后的代码以及去掉了注释的内容。
工具:通常用g++ -E
或cl /E
查看预处理输出。
2. 编译(Compilation)
在编译阶段,预处理后的C++代码会被编译器翻译成中间代码或汇编代码。具体步骤包括:
- 语法分析:编译器检查代码的语法是否正确。
- 语义分析:编译器检查代码的语义正确性,包括类型检查、函数调用的正确性等。
- 优化:编译器可能对代码进行优化,如消除死代码、内联函数等。
- 代码生成:编译器将源代码翻译为汇编代码,这是一种低级别的指令集代码,可以与底层硬件直接交互。
结果是每个源文件会生成一个对应的汇编文件。
工具:通常用g++ -S
生成汇编代码。
3. 汇编(Assembly)
汇编器将编译器生成的汇编代码转换成机器代码(也称为目标代码,即二进制文件),这些机器代码可以被计算机处理器直接执行。每个源文件经过汇编后,生成一个目标文件(以.o
或.obj
为后缀)。
目标文件包含机器指令和一些符号表,但它还不是一个完整的可执行文件,因为它可能会依赖于其他目标文件或库文件。
工具:通常用g++ -c
生成目标文件。
4. 链接(Linking)
链接器将多个目标文件以及需要的库文件(如标准库或第三方库)组合起来,生成一个完整的可执行文件。主要任务包括:
- 符号解析:链接器会处理函数和变量的引用,确保所有的符号(如函数调用或变量)都有定义。如果某个符号在一个目标文件中被使用,但定义在另一个文件中,链接器会将它们关联起来。
- 库链接:链接器会把标准库、第三方库或用户提供的库链接到可执行文件中。 “C++代码到可执行文件的生成过程中,链接阶段可以分为静态链接和动态链接。静态链接将所有库文件嵌入到可执行文件中,生成自包含的文件,文件较大,但运行时不依赖外部库;动态链接则只保留对库的引用,运行时从系统中动态加载库,文件较小,多个程序可以共享库,但运行时必须确保库的存在和正确版本。”
//此命令会将静态库`libmy_static_library.a`嵌入到`myprogram`中,生成自包含的可执行文件。
g++ -static -o myprogram main.cpp -lmy_static_library
//此命令生成的`myprogram`会在运行时动态加载库`libmy_dynamic_library.so`。
g++ -o myprogram main.cpp -lmy_dynamic_library
对比点 | 静态链接 | 动态链接 |
---|---|---|
库代码存储方式 | 库代码嵌入到可执行文件中 | 库代码在运行时动态加载 |
可执行文件大小 | 文件较大 | 文件较小 |
运行时依赖 | 不依赖外部库,独立运行 | 依赖外部库,运行时必须提供相应的库文件 |
内存占用 | 各程序独立加载库代码,内存占用较大 | 各程序可共享动态库,减少内存占用 |
性能 | 稍微好一些,因为不需要在运行时加载库 | 稍微差一些,因为运行时需要加载库 |
库更新的灵活性 | 需要重新编译程序 | 可以单独更新库,不需要重新编译程序 |
适用场景 | 稳定的、独立的环境,如嵌入式系统 | 程序运行环境可控,如服务器或桌面系统 |
- 生成可执行文件:链接器最终会将所有的目标文件合并成一个完整的可执行文件。
工具:g++
或ld
负责链接过程。
整个流程的总结
- 预处理:展开宏,处理
#include
等预处理指令。 - 编译:将C++源代码转换为汇编代码。
- 汇编:将汇编代码转换为机器代码(目标文件)。
- 链接:将目标文件和库文件合并,生成最终的可执行文件。
相关命令示例:
以g++
为例,编译一个C++文件:
g++ -o myprogram main.cpp
这条命令会经过预处理、编译、汇编和链接,最终生成可执行文件myprogram
。
1.1.8 说说static关键字的作用
面试简要回答:
“static 关键字在 C++ 中有多种作用:
局部变量中:声明的变量在函数多次调用间保持其值。
全局变量和函数中:限制作用域,使其只在声明所在的文件中可见。
类中:static 成员变量和成员函数属于类本身,所有对象共享,可以不依赖对象实例直接访问。”
static 关键字有多种作用,具体取决于它用于变量、函数或类中的位置。它的作用包括控制变量的存储方式、生命周期、作用域以及类成员的共享属性。
- 局部变量中的 static
当 static 用于函数中的局部变量时,该变量的存储方式和生命周期与普通局部变量不同。普通局部变量在函数调用时创建,函数返回时销毁。static 局部变量在函数中声明时,只初始化一次,并且它的值在函数多次调用之间保持不变。 - 全局变量中的 static
static 用在全局变量或全局函数前面时,可以限制它们的作用域,使其仅在声明所在的文件内可见。这意味着其他文件不能访问或引用这个变量/函数,起到了类似封装的作用。 - 类成员中的 static
当 static 用于类的成员变量或成员函数时,它的行为与普通类成员不同。static 成员属于类本身,而不是某个特定对象。
static 数据成员:类的所有对象共享同一个 static 成员变量。它的生命周期贯穿整个程序,且可以在没有创建对象的情况下通过类名访问。
static 成员函数:只能访问类的 static 成员变量,不能访问类的非 static 成员。它可以通过类名直接调用,无需实例化对象。
**当调用一个对象的非静态成员函数时,系统会把对象的起始地址赋值给成员函数的this指针。而静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针。**因为没有指向对象的 this 指针,静态成员函数无法直接访问对象的非静态成员(既包括非静态成员变量,也包括非静态成员函数)。只有静态成员可以直接访问。
1.1.9 数组和指针的区别
数组和指针在C/C++中有密切的关系,数组名可以看作是指向数组首元素的常量指针,但数组和指针的用法和内存管理方式有明显区别。数组具有固定大小并且在声明时分配内存,而指针则更灵活,可以指向不同的内存位置并动态分配或释放内存。
区别点 | 数组 | 指针 |
---|---|---|
定义 | 数组是固定大小的、存储同类型元素的集合。 | 指针是一个变量,存储另一个变量的内存地址。 |
存储空间 | 数组在声明时分配固定大小的连续内存空间。 | 指针只占用存储地址的大小(例如4或8字节)。 |
修改大小 | 数组的大小在定义时固定,不能在运行时修改。 | 指针可以通过动态内存分配来改变指向的数据大小。 |
内存位置 | 数组名代表数组第一个元素的地址,数组本身不可移动。 | 指针可以通过赋值或运算改变指向不同的地址。 |
数组名 vs 指针 | 数组名是常量,不能进行赋值操作。 | 指针是一个变量,可以指向不同的内存位置。 |
访问方式 | 通过下标访问数组元素,arr[i] 。 | 通过指针的解引用访问,*(ptr + i) 。 |
关系 | 数组名是数组首元素的指针,但数组和指针类型不同。 | 指针可以指向数组的第一个元素,也可以指向任意内存。 |
类型推导 | 数组名的类型是数组类型,如int[10] 。 | 指针类型明确,如int* ,代表指向整数的指针。 |
内存分配方式 | 数组的内存是自动或静态分配的,编译时分配。 | 指针可以通过malloc 、new 等动态分配内存。 |
传递给函数的方式 | 数组传递给函数时,传递的是指向第一个元素的指针。 | 指针可以直接传递给函数,或者指向任意位置。 |
1.1.10 什么是函数指针,如何定义函数指针,有什么使用场景?
函数指针 是一种指向函数的指针,它指向的是函数,而不是变量。通过函数指针,我们可以像调用普通函数一样调用指针所指向的函数。函数指针允许程序根据不同的需求动态选择要调用的函数,使代码更加灵活和模块化。通过函数指针,可以实现回调机制、动态函数调用、函数表、多态等功能,使得代码更加模块化和可扩展。
2. 如何定义函数指针?
定义函数指针的语法比较复杂,通常遵循以下格式:
返回类型 (*函数指针名)(参数类型列表);
返回类型
:函数指针指向的函数的返回类型。函数指针名
:指针变量的名称。参数类型列表
:函数指针指向的函数所需的参数列表类型。
例如,定义一个指向返回 int
类型,且带有两个 int
类型参数的函数指针:
int (*funcPtr)(int, int);
这个 funcPtr
是一个指向带有两个 int
参数并返回 int
值的函数的指针。
3. 如何使用函数指针?
3.1 函数指针的赋值
可以将已定义的函数赋值给函数指针。函数名其实是指向该函数的指针,因此可以直接赋值。
int add(int a, int b) {return a + b;
}
int (*funcPtr)(int, int) = add; // 将 add 函数的地址赋给函数指针
3.2 调用函数指针
调用函数指针时,可以像调用普通函数一样使用,但通过指针来进行函数的调用。
int result = funcPtr(3, 5); // 等同于调用 add(3, 5)
std::cout << result; // 输出 8
也可以通过显式解引用函数指针来调用:
int result = (*funcPtr)(3, 5); // 也可以这样写
3.3 示例
下面是一个完整的示例,展示如何定义、赋值和调用函数指针:
#include <iostream>int add(int a, int b) {return a + b;
}int subtract(int a, int b) {return a - b;
}int main() {int (*operation)(int, int); // 声明函数指针operation = add; // 将 add 函数赋值给函数指针std::cout << "Add: " << operation(5, 3) << std::endl; // 输出 8operation = subtract; // 将 subtract 函数赋值给函数指针std::cout << "Subtract: " << operation(5, 3) << std::endl; // 输出 2return 0;
}
4. 使用场景
4.1 回调函数
在某些场合,可能希望某个函数在执行完某些任务后,执行一个额外的操作。这个操作由用户决定,使用函数指针传递可以实现这种灵活性。典型的例子是事件驱动程序设计中的回调函数,例如在信号处理或UI编程中。
void performOperation(int x, int y, int (*operation)(int, int)) {std::cout << "Result: " << operation(x, y) << std::endl;
}int main() {performOperation(10, 5, add); // 回调 add 函数performOperation(10, 5, subtract); // 回调 subtract 函数return 0;
}
4.2 动态函数调用
在某些情况下,程序在运行时根据条件来决定调用哪个函数。使用函数指针,可以在程序运行时动态选择合适的函数。
例如,实现简单的菜单系统:
void menu() {int choice;std::cout << "1. Add\n2. Subtract\n";std::cout << "Enter your choice: ";std::cin >> choice;int (*operation)(int, int);if (choice == 1) {operation = add;} else if (choice == 2) {operation = subtract;}std::cout << "Result: " << operation(10, 5) << std::endl;
}
4.3 函数表
在编写编译器、解释器或模拟器时,通常会有不同的操作码对应不同的操作。使用函数指针表可以提高代码的灵活性和可维护性。
int (*operations[2])(int, int) = {add, subtract}; // 函数指针数组
std::cout << "Add result: " << operations[0](3, 2) << std::endl;
std::cout << "Subtract result: " << operations[1](3, 2) << std::endl;
4.4 面向对象编程的多态实现
在C++的面向对象编程中,函数指针的概念用于虚函数和多态的底层实现。虚函数表(V-Table)实际上是一个包含虚函数指针的表,允许程序在运行时根据对象类型动态调用合适的函数。
1.1.11 静态变量什么时候初始化?
类型 | C++中的静态变量 |
---|---|
局部静态变量 | - 初始化时机:第一次调用函数时 - 生命周期:贯穿整个程序,直到程序结束 |
全局静态变量 | - 初始化时机:程序启动时,在 main() 之前 - 生命周期:程序开始到结束,作用域限于文件 |
类的静态成员变量 | - 初始化时机:程序启动时,在类外定义并初始化 - 生命周期:所有对象共享,程序结束前有效,C++17支持 inline 初始化 |
静态链接/动态链接影响 | 静态链接时静态变量在加载时初始化;动态链接库中的静态变量在库加载时初始化 |
C++17 支持 inline 静态变量 | 支持 inline 静态成员变量,允许在类内直接初始化 |
说明:
- 局部静态变量:在函数内部声明并使用
static
修饰,首次调用时初始化,之后保持值不变。 - 全局静态变量:在文件作用域中定义,
static
修饰限定其作用范围在文件内,初始化在程序启动时完成。 - 类的静态成员变量:在类中声明,但必须在类外初始化,所有对象共享一个静态成员变量。
1.1.12 nullptr调用成员函数可以吗?为什么?
可以。因为在编译时对象就绑定了函数地址,和指针空不空没关系。
1.1.13 什么是野指针,怎么产生的,如何避免?
总结
野指针是指向无效内存区域的指针,可能引发未定义行为或程序崩溃。它并不为 nullptr,因此从表面上看像是有效的指针,但实际上它所指向的内存空间可能已经被释放、回收或从未正确初始化过。这类指针一旦被访问,可能会引发不可预测的行为,如程序崩溃、数据损坏等。
它通常由于未初始化的指针、释放后的指针继续使用、返回局部变量地址、多次释放等原因产生。
通过初始化指针为 nullptr、释放内存后将指针设为 nullptr、使用智能指针等方法,可以有效避免野指针问题。
1.1.14 静态局部变量、全局变量、局部变量的特点,以及使用场景。
变量类型 | 存储位置 | 生命周期 | 作用域 | 初始化 | 典型使用场景 |
---|---|---|---|---|---|
局部变量 | 栈区 | 函数调用时分配,函数返回时释放 | 函数或代码块内部 | 每次进入作用域时重新初始化 | 临时数据存储,避免全局污染,函数内部的计算 |
静态局部变量 | 静态数据区 | 程序运行期间(保留状态) | 函数或代码块内部 | 程序首次执行到时初始化 | 多次调用函数时需保存状态,缓存中间结果 |
全局变量 | 静态数据区 | 程序运行期间 | 文件内或全局(可跨文件共享) | 程序开始时初始化 | 跨函数共享数据,系统状态变量,全局配置 |
静态全局变量 | 静态数据区 | 程序运行期间 | 文件内部(仅限于声明所在文件) | 程序开始时初始化 | 文件内部共享数据,避免命名冲突和全局污染 |
解释:
- 局部变量:在函数内存储和使用,生命周期短,适合存放临时数据。
- 静态局部变量:尽管声明在函数内部,但具有全局生命周期,适合需要在多次函数调用间保留状态的场景。
- 全局变量:在整个程序中可访问,生命周期从程序启动到结束,用于存储跨函数或跨文件共享的数据。
- 静态全局变量:只在声明它的文件内有效,适用于需要限制作用域、避免命名冲突的全局数据。
1.1.15 内联函数和宏函数的区别?
内联函数和宏函数都是用于提高程序执行效率的手段,但它们的实现机制和使用场景存在明显的不同。以下是它们之间的主要区别:
区别点 | 内联函数 | 宏函数 |
---|---|---|
定义方式 | 使用 inline 关键字声明的函数 | 使用 #define 预处理器指令定义 |
作用机制 | 编译时通过函数调用替换为函数体代码 | 预处理阶段进行简单的文本替换 |
类型检查 | 编译器会进行类型检查,确保参数类型正确 | 无类型检查,只做简单的文本替换 |
调试支持 | 支持调试,可以在调试器中查看内联函数的调用堆栈 | 不支持调试,宏展开后很难追踪问题 |
参数求值 | 参数只被求值一次,正常按值传递 | 参数会被多次求值,可能导致副作用 |
作用域 | 受 C++ 作用域规则限制,遵循函数作用域规则 | 没有作用域的概念,全局可见,容易引发命名冲突 |
错误排查 | 编译时检查,错误更易排查 | 宏展开后的错误更难定位,因为仅在预处理阶段处理 |
扩展性 | 支持递归和复杂逻辑,且支持重载和模板 | 宏不支持递归、模板或重载功能,只能做简单替换 |
- 内联函数:
- 内联函数是在编译时通过将函数调用替换为函数体来避免函数调用开销,但它仍然是一个真正的函数,因此具备所有函数的特性,如类型检查、作用域控制和调试支持。编译器可以决定是否实际将函数内联,因此即使标记为
inline
,也不一定会内联。 - 适用于小型、频繁调用的函数。
- 内联函数是在编译时通过将函数调用替换为函数体来避免函数调用开销,但它仍然是一个真正的函数,因此具备所有函数的特性,如类型检查、作用域控制和调试支持。编译器可以决定是否实际将函数内联,因此即使标记为
- 宏函数:
- 宏函数是在预处理阶段展开,不进行任何类型检查和作用域控制,实际上是简单的文本替换,容易出现参数多次求值导致的副作用(如
#define SQUARE(x) ((x) * (x))
中x
可能被多次求值)。 - 由于没有类型检查,错误可能更难排查。宏的定义在整个程序中有效,可能引发命名冲突。
- 宏适合用于非常简单的操作,如常量定义或简单的表达式替换。
- 宏函数是在预处理阶段展开,不进行任何类型检查和作用域控制,实际上是简单的文本替换,容易出现参数多次求值导致的副作用(如
总结:
- 内联函数 更加安全,支持类型检查、调试和复杂逻辑,因此推荐在 C++ 中优先使用内联函数。
- 宏函数 虽然可以用于提高性能,但存在较多的隐患,特别是参数多次求值和缺乏类型检查的问题。
1.1.16 运算符i++和++i的区别
在 C++ 中,i++
和 ++i
都是对变量 i
进行自增操作(即将 i
的值加 1),但它们的工作机制和返回值有所不同。
区别总结:
运算符 | 操作顺序 | 返回值 | 效率 | 使用场景 |
---|---|---|---|---|
i++ | 先使用 i ,后自增 | 返回自增前的值 | 相对较慢,可能涉及临时对象创建 | 用在需要当前值的场景 |
++i | 先自增,后使用 i | 返回自增后的值 | 相对较快,不涉及临时对象的创建 | 用在仅需自增结果的场景 |
性能差异:
- 对于简单的基本数据类型(如
int
),性能差异不明显。 - 但对于复杂类型(如自定义类或容器的迭代器),
i++
可能涉及拷贝和临时对象的创建,而++i
更高效。
使用建议:
- 如果只需要自增后的结果,优先使用
++i
。 - 如果确实需要在自增前的值,才使用
i++
。
1.1.17 new和malloc的区别,各自底层实现的原理。
总结
new
是 C++ 特有的操作符,不仅分配内存,还会调用构造函数初始化对象。它的底层实现是调用operator new
分配内存,而operator new
通常通过malloc
或类似机制实现。malloc
是 C 语言的函数,它只负责分配内存,不会调用构造函数。
new
和 malloc
都是用于动态内存分配的操作,但它们在使用方式、底层实现和功能上有很大的区别。以下是它们的区别以及各自的底层实现原理:
new
和 malloc
的区别
特性 | new | malloc |
---|---|---|
语言 | C++ 特有 | C 和 C++ 都可以使用 |
调用构造函数 | 会调用构造函数来初始化对象 | 只分配内存,不调用构造函数 |
返回类型 | 返回的是正确的对象类型(不需要类型转换) | 返回 void* ,需要强制类型转换 |
内存分配失败 | 抛出 std::bad_alloc 异常 | 返回 NULL ,需要手动检查错误 |
内存释放 | 使用 delete 释放内存,调用析构函数 | 使用 free() 释放内存,只释放内存,不调用析构函数 |
重载 | 可以通过重载 new 操作符进行自定义分配行为 | 不支持重载 |
分配数组 | 可以用 new[] 来分配数组 | 只能通过 malloc 分配连续内存,不处理对象数组 |
底层实现原理
1. new
的底层实现原理
在 C++ 中,new
操作符不仅仅是分配内存,它还负责调用构造函数来初始化对象。
-
步骤 1:调用
operator new
分配内存:- 当使用
new
操作符时,它会首先调用全局的operator new
函数来分配足够的内存。这个函数通常是由标准库实现,类似于malloc()
,负责在堆上分配指定大小的内存。 - 全局
operator new
的实现 类似如下:void* operator new(size_t size) {if (void* ptr = malloc(size)) // 使用 malloc 分配内存return ptr;elsethrow std::bad_alloc(); // 分配失败时抛出异常 }
operator new
可以被重载,以自定义内存分配策略。
- 当使用
-
步骤 2:调用构造函数初始化对象:
- 内存分配完成后,
new
会调用对象的构造函数进行初始化。这是new
和malloc
之间最重要的区别之一。malloc
只是分配原始内存,而new
还会初始化对象。 - 示例:
MyClass* obj = new MyClass(5); // 调用构造函数 MyClass(5)
- 内存分配完成后,
-
步骤 3:返回正确类型的指针:
new
返回的指针是正确的对象类型的指针,而malloc
返回void*
指针,需要进行类型转换。
2. malloc
的底层实现原理
malloc
是标准 C 库函数,它只负责在堆上分配指定大小的原始内存,并不会做任何对象的初始化工作。
-
步骤 1:确定分配大小:
malloc
的输入是分配内存的字节数。它直接向系统申请分配内存块,通常通过调用底层的系统调用(如brk
或sbrk
)来调整程序的堆大小,或通过mmap
分配更大的内存块。- 示例:
void* ptr = malloc(sizeof(MyClass)); // 分配一块足够存储 MyClass 对象的内存
-
步骤 2:内存分配:
malloc
通过管理程序的堆区域,利用现有的空闲链表、块分配等技术来查找合适的内存块。如果内存块不足,它会向操作系统申请更多内存。- 如果分配成功,
malloc
返回指向分配内存的void*
指针。如果失败,返回NULL
。
-
步骤 3:无类型的原始内存:
malloc
返回的是无类型的void*
指针,因此在使用时需要强制类型转换。- 示例:
MyClass* obj = (MyClass*) malloc(sizeof(MyClass));
-
注意:
malloc
只分配内存,不会调用对象的构造函数进行初始化。如果要创建对象并初始化,用户需要手动使用构造函数(比如通过placement new
)。
内存释放的区别
delete
和free()
的区别:delete
:使用new
分配的内存需要通过delete
释放。delete
不仅会释放内存,还会调用对象的析构函数以正确销毁对象,处理资源释放。free()
:malloc
分配的内存需要通过free()
释放,free()
只会释放内存,不会调用对象的析构函数。
使用场景
-
new/delete
:- 适用于 C++ 对象的创建与销毁,尤其是在需要构造函数和析构函数自动管理资源的情况下。
- 示例:动态创建对象并进行初始化。
MyClass* obj = new MyClass(10); delete obj; // 自动调用析构函数
-
malloc/free
:- 适用于 C 语言的内存分配,或者在不需要构造函数的 C++ 项目中。
- 示例:动态分配内存,用于存储原始数据。
int* array = (int*) malloc(10 * sizeof(int)); free(array); // 释放内存
1.1.18 const和define的区别
下面是 const
和 #define
的详细对比表格,包含优缺点、使用场景以及编译处理差异。
const
和 #define
的对比
特性 | const | #define |
---|---|---|
语法 | const 是语言的一部分,属于 C/C++ 关键字 | #define 是预处理指令,不属于 C/C++ 语言本身 |
类型安全 | 有类型,编译器会进行类型检查 | 没有类型,仅做纯文本替换,不进行类型检查 |
作用域 | 受作用域规则限制,可以是局部或全局 | 没有作用域概念,全局有效,通常在文件范围内生效 |
编译时处理 | 编译时常量,编译器为其分配内存并优化 | 预处理阶段进行文本替换,不分配内存 |
调试支持 | 可以在调试器中查看 const 常量的值 | 由于是文本替换,#define 的值无法在调试器中查看 |
命名空间支持 | 支持命名空间,遵循 C++ 作用域规则 | 不支持命名空间,不遵循作用域规则 |
可变性 | 一旦初始化就无法更改 | 不能更改,因为它只是文本替换 |
内存开销 | 如果是全局 const ,会占用内存,但编译器可能优化 | 不占用内存,但可能会导致代码膨胀 |
复杂表达式 | 可以处理复杂类型和表达式 | 仅能进行简单的文本替换,复杂表达式可能产生问题 |
编译效率 | 编译时会进行类型检查和优化,效率较高 | 由于是预处理替换,编译器不会进行类型检查 |
优缺点
特性 | const | #define |
---|---|---|
优点 | - 类型安全,有编译器检查 - 支持调试器查看 - 支持作用域和命名空间 | - 简单快速 - 无内存占用 - 预处理阶段进行替换 |
缺点 | - 可能有内存开销 - 需要编译器进行更多优化 | - 无类型安全,容易出错 - 无法调试 - 全局作用域影响大 |
使用场景
使用场景 | const | #define |
---|---|---|
常量定义 | 当需要定义类型安全的常量时,例如函数参数、全局配置 | 定义简单的常量值(如常量替换,不关心类型) |
复杂表达式 | 处理复杂类型或表达式,如指针、数组、对象等 | 简单文本替换,无需复杂逻辑 |
调试与优化 | 需要进行调试、类型检查、或优化时 | 不需要类型检查,仅做简单的值替换 |
编译和处理过程
处理过程 | const | #define |
---|---|---|
编译过程 | 编译器进行类型检查并为其分配内存,同时进行优化 | 预处理器在编译之前进行文本替换,不做任何类型检查 |
内存管理 | 可能分配内存,但编译器可能会优化为纯常量处理 | 无需分配内存 |
效率影响 | 类型安全,编译器可根据常量进行优化 | 由于是文本替换,效率高,但缺乏类型安全性 |
1.1.19 C++中函数指针和指针函数的区别
在C++中,函数指针 和 指针函数 是两个不同的概念。它们在语法和功能上有所区别,下面分别解释它们的含义和区别。
3. 区别总结
特性 | 函数指针 | 指针函数 |
---|---|---|
定义 | 指向函数的指针,存储的是函数的地址 | 返回一个指针类型的函数,返回值是指针 |
语法 | 返回类型 (*指针名称)(参数类型) | 返回类型 *函数名称(参数类型) |
用途 | 用于动态调用函数、回调机制等 | 用于返回指针,如动态内存分配、引用局部变量等 |
调用方式 | 通过函数指针调用函数 | 通过指针函数的返回值操作指针 |
示例 | void (*funcPtr)(int) | int* getPointer() |
使用场景 | 回调机制、算法选择等场景 | 动态内存管理、复杂数据结构操作等 |
两者在语法结构上容易混淆,但概念上不同:函数指针是指向函数的指针、其指向一个函数,而指针函数是返回指针的函数,其返回值为指针。
1. 函数指针 (Pointer to Function)
- 定义:函数指针是指向一个函数的指针,通过它可以调用该函数。
- 语法:
返回类型 (*指针名称)(参数类型)
- 用途:可以通过函数指针调用不同的函数,常用于回调函数、动态函数调用等场景。
示例:
#include <iostream>void func(int x) {std::cout << "Value: " << x << std::endl;
}int main() {// 定义一个指向函数的指针void (*funcPtr)(int) = func;// 通过函数指针调用函数funcPtr(10); // 输出:Value: 10return 0;
}
解释:
void (*funcPtr)(int)
表示一个返回类型为void
,参数为int
的函数指针。- 通过
funcPtr(10)
调用函数func
,效果等同于直接调用func(10)
。
2. 指针函数 (Function Returning a Pointer)
- 定义:指针函数是返回一个指针类型的函数,即该函数的返回值是一个指针。
- 语法:
返回类型 *函数名称(参数类型)
- 用途:用于函数返回指针,例如动态内存分配时返回分配的内存地址。
示例:
#include <iostream>int* getPointer() {static int value = 42;return &value;
}int main() {// 调用指针函数,获取返回的指针int* ptr = getPointer();std::cout << "Value: " << *ptr << std::endl; // 输出:Value: 42return 0;
}
解释:
int* getPointer()
表示这是一个返回int
类型指针的函数。getPointer()
返回指向value
的指针,*ptr
通过该指针访问value
的值。
1.1.20 const int *a, int const *a, const int a, int *const a , const int * const a 分别是什么,有什么特点。
声明 | 含义 | 特点 |
---|---|---|
const int *a | a 是一个指向 const int 的指针 | - 可以修改指针指向 - 不能修改指向的值 |
int const *a | 同 const int *a | - 可以修改指针指向 - 不能修改指向的值 |
const int a | a 是一个 const int 常量 | - 不能修改变量 a 的值 |
int *const a | a 是一个指向 int 的常量指针 | - 不能修改指针指向 - 可以修改指向的值 |
const int *const a | a 是一个指向 const int 的常量指针 | - 不能修改指针指向 - 不能修改指向的值 |
详细解释:
const
的位置:无论const
放在类型的前面还是后面,效果是一样的。const int *a
和int const *a
是等效的,都是指针指向的值不可修改。- 指针的
const
性:当const
出现在*
之后时,意味着指针本身是常量,不能改变指针指向的地址;如果出现在类型前,则意味着指针指向的值不可修改。
这几种声明结合了const
和指针,影响了指针本身和指向内容的可变性。
1. const int *a
或 int const *a
- 含义:
a
是一个指向const int
的指针。指针指向的内容(即int
类型的值)是常量,不能通过该指针修改内容,但指针本身可以改变。 - 特点:
- 不能修改指向的值。
- 可以修改指针
a
使其指向其他地址。
示例:
const int *a;
int x = 10;
a = &x; // 可以修改指针,使其指向其他变量
*a = 20; // 错误!不能修改指向的值
2. const int a
或 int const a
- 含义:
a
是一个const int
类型的常量。即a
是一个整型常量,它的值不能被修改。 - 特点:
a
是一个常量,不能修改其值。
示例:
const int a = 10;
a = 20; // 错误!不能修改常量的值
3. int *const a
- 含义:
a
是一个指向int
类型的常量指针。指针本身是常量,不能修改指针的地址,但是可以修改指针指向的值。 - 特点:
- 可以通过指针
a
修改其指向的值。 - 不能修改指针
a
使其指向其他地址。
- 可以通过指针
示例:
int x = 10;
int *const a = &x;
*a = 20; // 可以修改指向的值
a = &y; // 错误!不能修改指针的指向
4. const int * const a
- 含义:
a
是一个指向const int
类型的常量指针。即指针本身和指针指向的内容都不能修改。 - 特点:
- 不能修改指向的值。
- 不能修改指针本身的指向。
示例:
int x = 10;
const int * const a = &x;
*a = 20; // 错误!不能修改指向的值
a = &y; // 错误!不能修改指针的指向
1.1.21 使用指针需要注意什么?
在使用指针时,需要注意以下几个重要事项,以避免常见错误和潜在的程序崩溃问题:
注意事项 | 问题描述 | 建议 |
---|---|---|
指针初始化 | 未初始化指针可能指向随机内存地址,导致未定义行为 | 在声明指针时,将其初始化为 nullptr |
避免野指针 | 指向已释放或未分配内存的指针,可能导致崩溃或数据损坏 | 释放内存后将指针设置为 nullptr |
指针越界 | 超过数组或动态内存范围,访问无效地址 | 确保访问指针时在合法范围内 |
释放动态分配的内存 | 未释放内存会导致内存泄漏 | 动态分配的内存使用完后应及时释放 |
避免多次释放 | 重复释放同一指针可能导致程序崩溃 | 释放后将指针置为 nullptr |
指针类型匹配 | 指针类型和实际对象类型不匹配,可能导致数据读取错误 | 确保指针类型与实际数据类型一致 |
避免悬空指针 | 指针指向已释放的内存,继续使用该指针会产生错误 | 释放后指针设为 nullptr ,避免访问已释放的内存 |
合理使用指针时,必须特别注意它的生命周期、内存管理和边界条件,避免常见的指针错误,确保程序的稳定性和安全性。
1.1.22 内联函数和函数的区别,内联函数的作用。
内联函数和普通函数的区别
区别点 | 内联函数 | 普通函数 |
---|---|---|
调用方式 | 编译时直接在调用处插入函数体的代码,避免函数调用的开销。 | 通过函数调用机制进行调用,存在参数压栈和返回地址的开销。 |
函数体替换 | 编译器将内联函数的代码在每个调用点直接展开,而不是跳转调用。 | 通过跳转调用,进入函数体执行后再返回调用处。 |
性能影响 | 减少了函数调用的开销(如压栈、跳转等),适合频繁调用的小函数。 | 函数调用有一定的开销,适合较大的、复杂的函数体。 |
代码大小 | 可能导致代码膨胀(code bloat),因为函数体被重复插入多次。 | 代码大小较小,因为函数体只定义一次,调用时通过跳转执行。 |
使用限制 | 不能用于递归函数、包含复杂循环或大量代码的函数。 | 适用于任何函数,包括递归和复杂的函数体。 |
调试 | 不容易调试,因为内联函数可能在编译时被展开,调试信息可能不准确。 | 调试方便,函数调用保留了明确的跳转地址和栈帧信息。 |
内联函数的作用
内联函数主要用于减少函数调用的开销,尤其是对频繁调用的小型函数。它的作用包括以下几点:
-
提升性能:通过在调用处直接展开函数体代码,避免了普通函数调用时的参数传递、栈帧保存、跳转和返回等开销。特别适用于频繁调用的小函数。
-
减少函数调用开销:普通函数调用时,涉及到压栈、跳转、返回等开销。而内联函数直接将函数体插入调用点,避免了这些开销,提高了执行效率。
-
增强代码可读性:内联函数可以替代宏定义,在保持代码简洁性的同时,内联函数具有类型安全性和语法检查,避免宏带来的隐患。
-
减少函数调用跳转:内联函数避免了跳转到函数地址执行的过程,尤其在频繁调用的小型函数上,可以显著提升性能。
内联函数的使用场景
- 适合小函数:例如简单的访问器(getter/setter)或数学计算函数等小型、频繁调用的函数。
- 适合频繁调用的函数:如果某个函数在代码中被多次调用,使用内联函数可以减少调用开销。
- 替代宏定义:内联函数提供了宏定义的功能,但同时具备函数的类型安全性和编译期检查,是宏的安全替代品。
内联函数的使用方式
inline int add(int a, int b) {return a + b;
}int main() {int result = add(3, 4); // 在编译时会直接插入add的函数体代码
}
在这个例子中,add
函数是一个内联函数,编译器可能会将它直接展开到 main
函数的调用处,从而避免函数调用的额外开销。
需要注意的是,内联只是建议,编译器不一定会将所有标记为 inline
的函数展开。
1.1.23 简述C++有几种传值方式,之间的区别是什么?
在 C++ 中,主要有三种传值方式:按值传递、按引用传递和按指针传递。它们之间的区别在于函数如何接收和操作参数的数据。以下是每种传递方式的简述及它们之间的区别:
三者的区别
传值方式 | 是否复制数据 | 是否能修改实参 | 效率 | 使用场景 |
---|---|---|---|---|
按值传递 | 是 | 否 | 较低(数据大时) | 小数据类型,只读参数,函数内部不修改外部数据 |
按引用传递 | 否 | 是 | 较高(无复制开销) | 大对象传递,修改外部数据,避免复制大量数据 |
按指针传递 | 否 | 是 | 较高(无复制开销) | 动态内存管理、数组传递,灵活传递 nullptr 指针做检查 |
1. 按值传递(Pass by Value)
- 机制:在函数调用时,将实参的值复制一份传递给函数。函数内部对参数的任何修改不会影响实参。
- 特点:
- 安全性高,函数无法修改原始数据。
- 适合传递简单的数据类型(如
int
、float
),但是对于大型数据结构(如对象、数组)效率较低,因为需要进行复制操作。
- 场景:用于函数内部只读参数,且数据量较小。
void func(int x) {x = 10; // 不会影响外部变量
}
2. 按引用传递(Pass by Reference)
- 机制:函数接收到的是实参的引用,也就是原始数据的别名。函数内部对参数的修改会直接影响实参。
- 特点:
- 没有复制数据的开销,适合传递大对象,效率较高。
- 函数可以修改实参,可能导致外部数据的不安全性。
- 需要注意函数对引用参数的修改。
- 场景:用于需要修改外部变量或避免大量数据复制的场景。
void func(int &x) {x = 10; // 会影响外部变量
}
3. 按指针传递(Pass by Pointer)
- 机制:函数接收的是指向实参的指针,使用指针间接操作实参。函数内部通过解引用指针来修改数据,修改后的值影响实参。
- 特点:
- 类似按引用传递,但更加灵活,可以通过传递
nullptr
进行参数控制。 - 需要管理指针的有效性,可能会产生空指针和悬空指针问题。
- 通常用于动态内存分配或数组传递。
- 类似按引用传递,但更加灵活,可以通过传递
- 场景:用于需要修改外部变量,或需要动态管理资源的场景。
void func(int *x) {if (x) {*x = 10; // 修改指针指向的实参}
}
4. 按常量引用传递(Pass by const Reference)
- 机制:类似于按引用传递,但通过
const
关键字保证函数内部无法修改参数的值。 - 特点:
- 避免数据复制开销,适合传递大对象。
- 函数内部无法修改参数,保证了参数的安全性。
- 场景:用于大对象的只读传递,既避免复制开销,又保护数据不被修改。
void func(const int &x) {// x 不能被修改
}
总结来说,选择传递方式时需要权衡数据的大小、是否需要修改实参以及性能要求。在处理较大数据结构时,引用和指针传递通常比按值传递效率更高,而按常量引用传递是保护数据的一种安全选择。
1.1.24 简述const* 和*const 的区别
const* 是常量指针; *const是指针常量.
区别总结:
语法 | 指向的值是否可变 | 指针本身是否可变 |
---|---|---|
const int *ptr | 否 | 是 |
int *const ptr | 是 | 否 |
const int *const ptr | 否 | 否 |
使用场景:
- 如果希望保护指向的对象,防止通过指针修改对象的值,则使用
const int *ptr
或int const *ptr
。 - 如果希望固定指针的指向,但允许修改指向的对象,则使用
int *const ptr
。 - 如果希望指针和指向的对象都保持不变,则使用
const int *const ptr
。
1. const int *ptr
(或者 int const *ptr
):
-
解释:指向
const
类型的指针。 -
特点:指针指向的内容不能修改,但指针本身可以改变,即可以让指针指向其他对象。
const int *ptr = &a; // 或者 int const *ptr = &a; *ptr = 5; // 错误!不能修改指向的值 ptr = &b; // 正确!可以修改指针的指向
总结:这里的
const
修饰的是指针指向的值,表示通过该指针不能修改其指向的对象,但指针本身可以指向其他地方。
2. int *const ptr
:
-
解释:指向不可变的指针。
-
特点:指针本身是
const
的,不能指向其他对象,但指针指向的内容可以修改。int *const ptr = &a; *ptr = 5; // 正确!可以修改指向的值 ptr = &b; // 错误!不能修改指针的指向
总结:这里的
const
修饰的是指针本身,表示该指针在初始化后不能再指向其他对象,但指针指向的对象可以被修改。
3. const int *const ptr
:
-
解释:指针本身和指针指向的内容都不可变。
-
特点:指针既不能指向其他对象,指向的内容也不能被修改。
const int *const ptr = &a; *ptr = 5; // 错误!不能修改指向的值 ptr = &b; // 错误!不能修改指针的指向
总结:
const
修饰了指针和指向的对象,表示指针的值和指向的对象都不能修改。