在 C++ 中,函数重载是指在同一个作用域中定义多个同名函数,这些函数通过参数的类型、数量或顺序的不同来区分。函数参数的顺序确实可以作为重载的条件之一。
函数重载的原则
C++ 允许通过以下几种方式来重载函数:
- 参数类型不同:函数的参数类型不同。
- 参数数量不同:函数的参数数量不同。
- 参数顺序不同:函数的参数类型相同,但顺序不同。
参数顺序的重载示例
以下是一个简单的示例,展示了如何通过参数的顺序来重载函数:
#include <iostream>
void func(int a, double b) {
std::cout << "Function with int and double: " << a << ", " << b << std::endl;
}
void func(double a, int b) {
std::cout << "Function with double and int: " << a << ", " << b << std::endl;
}
int main() {
func(5, 3.14); // 调用第一个函数
func(3.14, 5); // 调用第二个函数
return 0;
}
输出结果
Function with int and double: 5, 3.14 Function with double and int: 3.14, 5
在这个例子中,func
函数被重载了两次。第一个版本接受一个 int
和一个 double
,第二个版本接受一个 double
和一个 int
。根据参数的顺序不同,编译器能够正确地选择调用哪个版本的函数。
注意事项
- 类型必须不同:仅仅改变参数的顺序并不能使函数重载有效,参数的类型必须不同。
- 隐式类型转换 :如果参数类型可以通过隐式转换来匹配,则可能导致二义性。例如,如果有一个函数接受
int
和double
,而另一个接受double
和float
,则传入float
类型的参数可能导致二义性。 - 默认参数:如果函数使用了默认参数,可能会影响重载的决策,导致编译器无法确定调用哪个函数。
总结
在 C++ 中,函数参数的顺序可以作为重载的条件之一。通过不同的参数顺序,可以定义多个同名函数,从而提高代码的灵活性和可读性。然而,在使用函数重载时,需要注意参数类型、数量和顺序的组合,以避免二义性和潜在的错误。
归并排序的非递归实现
归并排序的非递归实现原理如下:
归并排序的非递归实现通过逐步合并已排序的子数组来完成排序。以下是主要的步骤和原理:
-
初始状态:将待排序数组视为多个长度为 1 的子数组(每个元素自成一个子数组)。
-
合并过程:逐步合并相邻的子数组,合并的过程是将两个已排序的子数组合并成一个更大的已排序子数组。
-
迭代:每次合并的步长(即合并的子数组的长度)逐渐加倍,直到整个数组合并成一个已排序的数组。
步骤
以下是非递归归并排序的具体步骤:
-
初始化 :设定一个步长
size
,初始值为 1。 -
合并过程:
- 在每个迭代中,使用
size
来决定要合并的子数组的大小。 - 对于数组的每个索引
i
,将arr[i]
到arr[i + size - 1]
和arr[i + size]
到arr[i + 2*size - 1]
进行合并。 - 这个过程需要确保合并的范围在数组的边界内。
- 在每个迭代中,使用
-
更新步长 :每次合并完成后,将
size
乘以 2,以便下次合并更大的子数组。 -
结束条件 :当
size
大于数组的长度时,排序完成。
示例
假设我们有一个数组 [38, 27, 43, 3, 9, 82, 10]
,我们可以通过以下步骤进行非递归归并排序:
- 初始数组:
[38, 27, 43, 3, 9, 82, 10]
- 第一次合并(
size = 1
):- 合并
[38]
和[27]
得到[27, 38]
- 合并
[43]
和[3]
得到[3, 43]
- 合并
[9]
和[82]
得到[9, 82]
- 合并
[10]
得到[10]
- 结果:
[27, 38, 3, 43, 9, 82, 10]
- 合并
- 第二次合并(
size = 2
):- 合并
[27, 38]
和[3, 43]
得到[3, 27, 38, 43]
- 合并
[9, 82]
和[10]
得到[9, 10, 82]
- 结果:
[3, 27, 38, 43, 9, 10, 82]
- 合并
- 第三次合并(
size = 4
):- 合并
[3, 27, 38, 43]
和[9, 10, 82]
得到[3, 9, 10, 27, 38, 43, 82]
- 合并
- 完成排序。
下面是实现这个代码的注意事项:
在上面的示例中当size=2的时候可以看到是进行了差错处理的,合并不同区间时出现了越界情况,这个差错处理就是我们要注意的点,在使用非递归实现的归并排序中会遇到下面几种越界的情况:
这里假设第一组的开始和结束坐标为bgein1和end1,第二组为begin2和end2 上面的例子还少了一种是连end1也越界了,对于这些情况有两种解决方法:这里介绍一种就是对于越界的下标进行调整即可:
下面是代码:
cpp
void MergeSortNor3(std::vector<int>& nums) {
int n = nums.size();
// 创建一个临时数组
std::vector<int> tmp(n);
int gap = 1; // 确定每组的元素个数
while (gap < n) {
int j = 0;
for (int i = 0; i < n; i += 2 * gap) {
// 确定左右区域的开始和结束区间
int begin1 = i;
int end1 = std::min(i + gap - 1, n - 1);
int begin2 = i + gap;
int end2 = std::min(i + 2 * gap - 1, n - 1);
// 处理越界情况
if (end1 >= n) { // 情况一:end1越界
end1 = n - 1;
begin2 = n; // 指向不存在的下标范围
end2 = n;
} else if (begin2 >= n) { // begin2和end2越界
end1 = n - 1;
begin2 = n;
end2 = n;
} else if (end2 >= n) { // end2越界
end2 = n - 1;
}
// 合并过程
while (begin1 <= end1 && begin2 <= end2) {
if (nums[begin1] < nums[begin2]) {
tmp[j++] = nums[begin1++];
} else {
tmp[j++] = nums[begin2++];
}
}
// 处理剩余元素
while (begin1 <= end1) {
tmp[j++] = nums[begin1++];
}
while (begin2 <= end2) {
tmp[j++] = nums[begin2++];
}
}
// 将合并后的结果复制回原数组
std::memcpy(nums.data(), tmp.data(), sizeof(int) * n);
gap *= 2;
}
}
不能进行运算符重载的操作符
在 C++ 中,虽然大多数操作符都可以被重载,但有一些操作符是不能被重载的。以下是不能进行重载的操作符列表:
-
范围解析运算符 (
::
)- 用于访问类的静态成员或命名空间中的成员。
-
成员访问运算符 (
.
)- 用于访问对象的成员。
-
指针成员访问运算符 (
->
)- 用于访问指针指向对象的成员。
-
条件运算符 (
?:
)- 三元运算符,用于条件判断。
-
sizeof 运算符
- 用于获取类型或对象的大小。
-
类型id 运算符 (
typeid
)- 用于获取对象的运行时类型信息。
-
常量运算符 (
const
、volatile
)- 这些关键字用于修饰类型,不能被重载。
-
new
和delete
运算符- 虽然可以重载
new
和delete
,但不能重载它们的全局版本(即不能更改它们的基本行为)。
- 虽然可以重载
-
new[]
和delete[]
运算符- 同样,虽然可以重载,但不能重载全局版本。
-
#
和##
预处理操作符- 这些是预处理器操作符,用于宏定义,不能被重载。
POD类
POD类(Plain Old Data)是C++中的一个术语,用于描述一种简单的数据结构。这种数据结构的特征是它只包含基本数据类型(如整型、浮点型、指针等)或者其他POD类型的成员,并且遵循特定的规则。POD类通常用于需要直接内存布局、简单的序列化和反序列化,以及与C语言代码的兼容性等场景。
POD类的特征
-
没有用户定义的构造函数、析构函数或拷贝控制函数:
- POD类不能有自定义的构造函数、析构函数或拷贝构造函数。它们只能使用编译器提供的默认构造函数和拷贝构造函数。
-
所有非静态数据成员都是POD类型:
- POD类的所有非静态数据成员必须是POD类型(如基本数据类型、其他POD类、指针等)。
-
没有虚函数:
- POD类不能有虚函数,因为虚函数会引入额外的复杂性和内存布局问题。
-
没有基类:
- POD类不能从其他类继承(即不能有基类)。
C++11及之后的变化
在C++11中,POD类的定义变得更加正式,分为两个类别:
-
标准布局类型(Standard Layout Type):
- 这些类型的内存布局符合特定的标准,允许它们在不同的编译器和平台之间保持一致。
-
Trivial类型:
- 这些类型具有简单的构造、拷贝和析构行为。具体来说,Trivial类型的构造函数、拷贝构造函数和析构函数都是简单的(即编译器生成的默认函数)。
一个类型如果是POD类型,它必须同时是标准布局类型和Trivial类型。
c++中struct和union的区别,如何使用union做优化
在 C++ 中,struct
和 union
都是用户定义的数据类型,但它们在内存布局和使用场景上有显著的区别。
-
内存分配:
struct
:每个成员都有自己的内存空间,struct
的大小是所有成员大小之和加上可能的对齐填充。union
:所有成员共享同一块内存,union
的大小是其最大成员的大小。也就是说,union
只能同时存储一个成员的值。
-
成员访问:
struct
:可以同时访问所有成员。union
:只能访问最后写入的成员,访问其他成员的值是不确定的(未定义行为)。
-
构造与析构:
struct
:可以有构造函数和析构函数,所有成员都可以被初始化。union
:在 C++11 及以后的版本中,union
可以有构造函数和析构函数,但只能有一个活跃的成员,构造和析构需要手动管理
最后是用途上:
struct一般用于逻辑上关联的不同数据存储,而union通常用于节省内存空间,作为一个优化项使用。例如在嵌入式系统或资源有限的场景中。通过让变量共用内存,可以减少内存消耗。很多底层库为了性能极致,也会使用union,我们如果开发业务层代码,建议直接使用struct,好用且不容易出bug。
下面是使用union进行优化的一个代码:
cpp
struct DataPacket {
int type;
union {
int intData;
float floatData;
char charData[4];
} data;
};
DataPacket packet;
// 用于指示数据类型
packet.type = 0; // 0 表示整数,1 表示浮点数,2表示字符数组
packet.data.intData = 10;
std::cout << "intData: " << packet.data.intData << std::endl;
// 再更改为浮点数数据
packet.type = 1;
packet.data.floatData = 5.5;
std::cout << "floatData: " << packet.data.floatData << std::endl;
在这个例子中,通过使用结构体中的union来表示多种数据类型,我们可以动态切换数据类型,同时节省内存的开销。
最后是使用union的注意事项:
使用union时要特别小心,不要在不确定某个成员是否有效的情况下,对改成员进行访问
在实际的开发中最好使用enum来标记当前union中有效的成员变量是哪一个
C++中使用using和typedef的区别是什么
- typedef主要用于给其它类型定义别名,而不能给模板定义别名
- using可以取代typedef的功能,并且语法较为简洁,可读性也更强
- using可以给模板定义别名
- using还可以用于命名空间的引入
cpp
template<typename T>
using Vec = std::vector<T>;
上面就是使用using给一个模板进行别名的设定
下面是使用using完成对一个命名空间引入的代码:
cpp
namespace LongNamespaceName {
int value;
}
using LNN = LongNamespaceName;
LNN::value = 42; // 相当于 LongNamespaceName::value
在C++11之后很多代码规范建议优先使用using而不是typedef。
C++中的enum和enum class的区别
enum class是强枚举类型,两者的区别主要在作用域和类型安全上
1.作用域
enum:枚举成员是直接进入enum的作用域中,也就是说在定义了枚举之后,可以直接使用枚举成员而不需要使用前缀
enum class:枚举成员只能通过显示的指定它们的枚举类型来进行访问,也就是要使用枚举名作为前缀,类似于作用域解析
2.类型安全
enum:传统枚举类型不安全,枚举成员会隐式转换为整型
enum class:强枚举类型是类型安全的,枚举成员不能隐式转换为其它类型,必须显示转换
代码例子:
cpp
// 传统枚举
enum Color {
RED,
GREEN,
BLUE
};
// 强类型枚举
enum class ColorClass {
RED,
GREEN,
BLUE
};
// 使用示例
int main() {
// 对于传统枚举
Color c = RED; // 直接访问,不需要前缀
int value = GREEN; // 可能的隐式转换
// 对于强类型枚举
ColorClass cc = ColorClass::RED; // 需要前缀
// int value = ColorClass::GREEN; // 错误,不能隐式转换,需要显式转换
return 0;
}
C++中default和delete关键字的区别
1. default
关键字
default
关键字用于指示编译器生成默认的特殊成员函数。通常情况下,编译器会自动生成这些函数,但在某些情况下,我们可能会显式地要求使用默认实现。
用法:
cpp
class MyClass {
public:
MyClass() = default; // 使用默认构造函数
MyClass(const MyClass&) = default; // 使用默认拷贝构造函数
MyClass& operator=(const MyClass&) = default; // 使用默认拷贝赋值运算符
~MyClass() = default; // 使用默认析构函数
};
作用
- 清晰性 :通过使用
default
,可以明确表示希望使用编译器生成的默认实现,而不是自己定义一个。 - 控制:在某些情况下,你可能会定义其他构造函数或析构函数,但仍然希望保留默认的拷贝构造函数或拷贝赋值运算符。
2.delete
关键字
delete
关键字用于显式地禁止某些特殊成员函数的使用。通过将某个函数标记为delete
,你可以防止该函数被调用,从而限制类的某些操作。
用法:
cpp
class MyClass {
public:
MyClass() = default; // 默认构造函数
MyClass(const MyClass&) = delete; // 禁止拷贝构造函数
MyClass& operator=(const MyClass&) = delete; // 禁止拷贝赋值运算符
~MyClass() = default; // 默认析构函数
};
作用
- 禁止拷贝 :通过将拷贝构造函数和拷贝赋值运算符标记为
delete
,可以防止对象被拷贝。这在需要确保对象唯一性或管理资源(如文件句柄、网络连接等)时非常有用。 - 控制对象的行为:可以通过删除某些操作来控制对象的使用方式,确保对象在使用时符合特定的语义。
- unique_ptr的原理就是使用delete禁用了拷贝构造和拷贝赋值运算符
扩展知识:
除了可以禁用特定的默认成员函数,delete 还可以用来禁用某些传统函数的重载。 例如,你可能不希望一个整数被隐式地转换为你的类类型。那么就可以使用delete来实现:
cpp
class MyClass {
public:
MyClass(int value) = delete; // 禁用带一个整数参数的构造函数
};
结合 delete 和 default 的构造更安全的类
通过合理地组合 delete 和 default,你可以更好地控制类的行为和接口,防止编写不安全的代码。
cpp
class NonCopyable {
public:
NonCopyable() = default; // 默认构造函数
NonCopyable(const NonCopyable&) = delete; // 禁用拷贝构造函数
NonCopyable& operator=(const NonCopyable&) = delete; // 禁用拷贝赋值运算符
};
C++中list的使用场景
数组和链表的区别想必大家都知道,而 list 就是双向链表。它适用于频繁插入和删除的场景,尤其是插入和删除操作多于遍历操作的场景,插入和删除操作的时间复杂度是 O(1)。
扩展知识
- 与其他容器比较: list 与 vector 和 deque 等其它容器各有优缺点。例如:
- vector 更适用于频繁访问和修改元素,但在中间插入和删除时效率较低。
- deque 特点是双端快速插入和删除,同时支持随机访问。
- set 和 map 之类的关联容器可以进行快速查找(基于平衡二叉树),但不适合频繁修改结构。
-
排序: 注意 list 的排序应该使用 list 自己的类成员 sort 函数,不应该使用 std::sort() 函数。
-
专用成员函数: list 还提供了一些独有的成员函数,比如 splice、merge、reverse、sort 等:
- splice:可以快速将某段元素移动到另一个 list 位置。
- merge:合并两个有序链表。
- reverse:反转链表元素。
- sort:对链表进行排序。
- 迭代器的使用: 由于 list 是双向链表,双向迭代器是最常用的迭代器类型,它可以向前和向后遍历容器。随机访问迭代器不能用于 list。
C++中lock_guard和unique_lock的区别
两者都是 RAII (资源获取即初始化)形式的锁管理类,用于管理互斥锁(mutex)。不过它们有一些关键区别:
-
lock_guard 是一个简单且轻量级的锁管理类。在构造时锁定给定的互斥体,并在销毁时自动解锁。它不可以显式解锁,也不支持锁的转移。
-
unique_lock 提供了更多的灵活性。它允许显式的锁定和解锁操作,还支持锁的所有权转移。unique_lock 可以在构造时选择不锁定互斥体,并在稍后需要时手动锁定。
lock_guard在使用上更加简洁,也因此它的唯一作用就是确保在作用域结束时释放锁,所以在性能上更加具有优势。
unique_lock 提供了更灵活的锁管理方式,适用于需要延迟锁定、显式解锁和锁所有权转移的场景。以下是一些特性和用法:
- 延迟锁定:你可以在构造 unique_lock 时选择不锁定互斥锁,而在后续调用 lock() 方法时显式锁定。
代码
cpp
std::mutex mtx;
void example() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 在需要时显式锁定
lock.lock();
// 互斥锁已经锁定,可以安全地访问共享资源
} // 作用域结束,mtx 自动解锁
- 显式解锁:你可以在中间需要时显式解锁互斥锁,然后再次锁定。
代码:
cpp
std::mutex mtx;
void example() {
std::unique_lock<std::mutex> lock(mtx);
// 访问共享资源
lock.unlock();
// 互斥锁已解锁
// 其他不能并发访问的操作
lock.lock();
// 再次锁定共享资源
} // 作用域结束,mtx 自动解锁
锁所有权转移:unique_lock 的所有权可以在不同作用域之间转移,这在一些需要精细控制锁生命周期的场景中非常有用。
cpp
std::mutex mtx;
void example() {
std::unique_lock<std::mutex> lock1(mtx);
// 访问共享资源
std::unique_lock<std::mutex> lock2 = std::move(lock1);
// lock1 不再拥有互斥锁
// lock2 拥有互斥锁
} // 作用域结束,mtx 自动解锁(如果 lock2 尚未解锁)
总结:lock_guard 适合简单的场合,不需要复杂的锁定/解锁逻辑,性能更好;而 unique_lock 提供了更多的灵活性,适合更复杂的并发编程需求,性能相对一般。