右值引用的应用与优点
-
应用:
- 右值引用用于实现移动语义,避免不必要的数据复制。它特别适合于处理临时对象和大型资源管理(如动态分配内存、文件句柄等)。
- 使用右值引用的主要场景是在自定义类的移动构造函数和移动赋值运算符中。
-
优点:
- 性能提升:通过移动资源而不是拷贝资源,减少了内存分配和数据复制的开销。
- 支持完美转发:结合
std::forward
可以在模板中实现参数的完美转发,从而保持传入参数的值类别。 - 简化资源管理:右值引用使得编写高效的资源管理类变得更加容易,比如实现一个自定义的容器或智能指针。
Lambda表达式
-
定义:Lambda表达式是一种能够定义匿名函数对象的方式,可以用来捕获外部变量并形成闭包。
-
基本语法:
[capture](parameters) -> return_type {// function body }
- 示例:
auto add = [](int a, int b) { return a + b; };
-
用途:
- 常用于 STL 算法,如
std::sort
、std::for_each
等。 - 用作回调函数,提高代码的可读性和简洁性。
- 常用于 STL 算法,如
空类的占用的内存
- 在C++中,一个空类至少占用1个字节的内存。这是为了确保每个对象都有独特的地址,即便它不包含任何数据成员。这也符合C++标准中定义的对象大小至少为1字节的要求。
结构体的占用的字节数
- 和类一样,C++中的结构体默认占用至少1个字节,其实际占用的字节数会因对齐和填充的需要而有所不同。如果结构体中有成员,则其大小为所有成员大小之和加上可能的填充字节。
虚函数与虚表
-
虚函数:允许子类重写父类的方法,实现多态。只有当基类中定义了虚函数,派生类才能重写该函数。
-
虚表:每个含有虚函数的类都有一个虚表(vtable),其中存储着指向该类的虚函数的指针。每个类的实例有一个指向该虚表的指针(即虚指针 vptr),用于在运行时确定调用哪个函数。
虚析构函数的作用
- 作用:
- 当通过基类指针删除派生类对象时,如果基类的析构函数不是虚的,那么只会调用基类的析构函数,这可能导致派生类的资源没有被释放,产生内存泄漏。
- 使用虚析构函数可以确保当删除基类指针指向的派生类对象时,会正确地调用派生类的析构函数,从而释放派生类所占用的资源。
传值和传址,深复制和浅复制
-
传值:
- 将对象的副本传递给函数。在此情况下,函数内部对参数的修改不会影响原对象。
-
传址:
- 传递对象的地址(指针或引用),允许函数直接操作原对象,函数内部的修改会影响原对象。
-
深复制:
- 复制对象及其指向的动态分配内存,确保两个对象之间完全独立。通常在类中实现自定义的拷贝构造函数和赋值运算符。
-
浅复制:
- 只复制对象的基本信息,指向同一块内存。当一个对象被修改或析构时,另一个对象也可能受到影响,导致未定义行为或资源冲突。
以下是有关C++的一些面试问题的详细回答:
前置++和后置++的本质区别与原理
-
前置++ (
++i
):- 操作:首先将
i
的值增加1,然后返回i
的新值。 - 原理:直接在原对象上进行操作,通常通过引用或指针返回。
- 操作:首先将
-
后置++ (
i++
):- 操作:首先返回
i
的当前值,然后将i
的值增加1。 - 原理:为了实现这个功能,后置++ 通常会创建一个临时对象来保存原始值,返回该对象,并在完成后再增加
i
的值。
- 操作:首先返回
-
总结:
- 前置++更高效,因为它不需要创建临时对象,而后置++则需要额外的内存开销。
map与set的原理及其他容器
-
map:
- 通常实现为红黑树(平衡二叉搜索树),存储键值对,提供O(log n)的查找、插入和删除性能。
- 关键特点:每个键唯一,自动排序。
-
set:
- 类似于map,但只存储唯一的元素,通常也是基于红黑树实现。
- 提供O(log n)的查找、插入和删除,且元素自动排序。
-
其他容器:
- vector:动态数组,支持随机访问,增长时可能涉及内存重新分配。
- deque:双端队列,支持两端插入和删除。
- list:双向链表,支持快速插入和删除,但不支持随机访问。
C++内存空间划分,值类型和引用类型,C#的垃圾回收原理
-
内存空间划分:
- 堆:用于动态分配的内存,通过
new
和delete
管理。 - 栈:用于局部变量的存储,自动管理内存的分配和释放。
- 全局/静态存储区:存放全局变量和静态变量,程序运行期间存在。
- 常量存储区、代码区。
- 堆:用于动态分配的内存,通过
-
值类型和引用类型:
- 值类型:直接存储数据,例如基本数据类型(int, char等)和结构体。
- 引用类型:存储对数据的引用,如指针和引用。对引用类型的修改会影响原始对象。
-
C#的垃圾回收原理:
- C#的垃圾回收(GC)自动管理堆上的内存,通过可达性分析算法确定哪些对象不再被使用,并回收它们占用的内存。GC使用代际假说来优化性能,优先回收较年轻代中的对象。这一机制减少了内存泄漏的风险,并简化了内存管理。
new与delete的本质及与malloc和free的区别
-
new:
- 在堆上分配内存,同时调用构造函数。
- 示例:
MyClass* obj = new MyClass();
-
delete:
- 调用析构函数并释放内存。
- 示例:
delete obj;
-
malloc:
- 仅申请指定大小的内存,不调用构造函数。
- 示例:
void* ptr = malloc(sizeof(MyClass));
-
free:
- 仅释放内存,不调用析构函数。
- 示例:
free(ptr);
-
区别总结:
new
和delete
是C++运算符,可以自动调用构造/析构函数;而malloc
和free
是C标准库函数,仅负责内存分配和释放,不处理对象生命周期。
内存泄漏的原因及避免方法
-
造成原因:
- 动态分配内存但未及时释放,例如忘记调用
delete
或free
,造成程序占用内存逐渐增大。
- 动态分配内存但未及时释放,例如忘记调用
-
避免方法:
- 使用智能指针(如
std::unique_ptr
和std::shared_ptr
)自动管理资源的生命周期,确保在超出作用域时自动释放内存。 - 定期检查代码中是否存在动态分配的内存未释放的情况,可以使用工具如Valgrind进行内存使用分析。
- 使用智能指针(如
以下是针对C++面试问题的详细回答:
智能指针有哪些
C++11引入了几种智能指针,主要包括:
-
std::unique_ptr
:- 独占式智能指针,每个指针只能由一个
unique_ptr
拥有,不能被复制,可以通过std::move
进行转移。 - 会自动在指针超出作用域时释放内存。
- 独占式智能指针,每个指针只能由一个
-
std::shared_ptr
:- 共享式智能指针,允许多个
shared_ptr
共享同一块内存。使用引用计数管理资源的生命周期,当最后一个shared_ptr
被销毁时,内存才会释放。
- 共享式智能指针,允许多个
-
std::weak_ptr
:- 弱引用智能指针,与
shared_ptr
配合使用,不增加引用计数,防止循环引用的问题。可以从一个shared_ptr
获得,其目的在于观察而不控制对象的生命周期。
- 弱引用智能指针,与
动态链接库和静态链接库的区别
-
静态链接库(Static Library):
- 在编译时将库的代码直接与应用程序的代码连接到一起。生成的可执行文件(.exe)包含所有必要的库代码。
- 优点:无需依赖外部库文件,部署过程简单。
- 缺点:可执行文件体积较大,更新库时需重新编译应用程序。
-
动态链接库(Dynamic Link Library, DLL):
- 在运行时加载,程序可以在运行时调用DLL中的函数。DLL文件与可执行文件分开,多个应用可以共享同一个DLL。
- 优点:可减小可执行文件体积,便于版本更新和共享。
- 缺点:需确保DLL文件存在且版本兼容。
-
关于EXE与链接:
- 一般情况下,生成的
.exe
文件是静态链接的,而.dll
文件是动态链接的。这意味着EXE在编译时已将所需的库代码集成,但在运行时可能仍然可以调用动态库。
- 一般情况下,生成的
二维指针
- 二维指针通常表示一个指向指针的指针,例如:
int **p
可以用来表示一个二维数组或数组的数组。 - 实际上,二维指针在内存中是一个指向指针数组的指针,每个指针又指向一个具体的行数据。
int rows = 5;
int cols = 4;
int **array = new int*[rows]; // 创建一维指针数组,用于指向每行
for (int i = 0; i < rows; i++) {array[i] = new int[cols]; // 为每行分配列数
}
// 使用完后记得释放内存
for (int i = 0; i < rows; i++) {delete[] array[i];
}
delete[] array;
指针和引用的区别
-
定义方式:
- 指针使用
*
符号定义,如int *ptr;
- 引用则使用
&
符号定义,如int &ref = var;
- 指针使用
-
空值:
- 指针可以为
nullptr
或指向某个有效地址。 - 引用必须在初始化时绑定到某个对象,不能为null。
- 指针可以为
-
重新赋值:
- 指针可以重新赋值,指向不同的对象。
- 引用一旦绑定到某个对象后就不可改变,如要改变需要使用新的引用。
-
语法:
- 使用指针时需要解引用操作符
*
,如*ptr
。 - 使用引用时不需要解引用,直接使用引用名即可。
- 使用指针时需要解引用操作符
宏定义,常见的宏指令
-
#define
:- 定义宏常量或宏函数。例如:
#define PI 3.14
或#define SQUARE(x) ((x)*(x))
- 定义宏常量或宏函数。例如:
-
#ifdef
/#ifndef
:- 用于条件编译,即根据是否定义了某个宏来决定是否编译某段代码。
- 例如:
#ifdef DEBUG std::cout << "Debug mode" << std::endl; #endif
-
#if
/#else
/#elif
:- 也是用于条件编译,根据表达式的值来选择编译哪部分代码。
-
#include
:- 引入头文件
-
#undef
:- 用于取消之前定义的宏。例如:
#undef PI
取消对PI
的定义。
- 用于取消之前定义的宏。例如:
-
#pragma
:- 用于向编译器发送特殊指令,可以控制编译器的某些行为,如警告等级、优化选项等。例如:
#pragma once // 防止头文件被多次包含
- 用于向编译器发送特殊指令,可以控制编译器的某些行为,如警告等级、优化选项等。例如:
-
#line
:- 用于改变编译器报告的行号和文件名,主要用于调试目的。
-
#error
:- 触发编译错误,可以在特定条件下提示开发者。例如:
#ifdef DEBUG #error "Debug mode is enabled!" #endif
- 触发编译错误,可以在特定条件下提示开发者。例如:
null 与 nullptr 的区别
-
null
:null
是一个常量,通常用于表示空指针。在 C++ 中,null
实际上是一个宏,代表整数常量 0。使用null
时可能会引发类型不明确的问题,因为它可以隐式转换为任何指针类型。
-
nullptr
:- C++11 引入的新关键字,用于表示空指针。与
null
不同的是,nullptr
是一个类型安全的指针常量,不能隐式转换为整数或其他类型。这使得在函数重载中更清晰,加上nullptr
使得代码在处理空指针时更加安全。
- C++11 引入的新关键字,用于表示空指针。与
int* p1 = nullptr; // 正确
int* p2 = null; // 在C++标准中,推荐使用nullptr,null可能会导致警告