龙岗企业网站设计公司,做网站公司郑州郑州的网站建设公司排名,php网站的特点,商标被注册了做网站构造函数和析构函数
构造函数不能是虚函数#xff0c;而析构函数可以是虚函数。原因如下#xff1a;
构造函数不能是虚函数#xff0c;因为在执行构造函数时#xff0c;对象还没有完全创建#xff0c;还没有分配内存空间#xff0c;也没有初始化虚函数表指针。如果构造…构造函数和析构函数
构造函数不能是虚函数而析构函数可以是虚函数。原因如下
构造函数不能是虚函数因为在执行构造函数时对象还没有完全创建还没有分配内存空间也没有初始化虚函数表指针。如果构造函数是虚函数就需要通过虚函数表来调用但是此时虚函数表还不存在无法找到正确的构造函数。构造函数也不需要是虚函数因为虚函数的作用是实现多态性即通过父类的指针或引用来调用子类的成员函数。而构造函数是在创建对象时自动调用的不可能通过父类的指针或引用去调用因此也就没有多态性的需求。析构函数可以是虚函数而且当要使用基类指针或引用删除子类对象时最好将基类的析构函数声明为虚函数否则会存在内存泄漏的问题。 这是因为如果不使用多态性那么编译器总是根据指针的类型来调用类成员函数。如果一个基类指针指向一个子类对象那么删除这个指针时只会调用基类的析构函数而不会调用子类的析构函数。这样就会导致子类对象中分配的资源没有被释放造成内存泄漏。如果将基类的析构函数声明为虚函数那么在删除基类指针时就会根据指针实际指向的对象类型来调用相应的析构函数。这样就可以保证子类对象中的资源也被正确地释放避免内存泄漏。
智能指针
智能指针是一种封装了原始指针的类对象可以自动管理指针所指向的资源的生命周期避免内存泄漏和空悬指针等问题。C 标准库提供了三种智能指针std::unique_ptrstd::shared_ptr 和 std::weak_ptr。另外C98 中还有一种智能指针 std::auto_ptr但是在 C11 中已经被废弃不建议使用。
std::unique_ptr 是一种独占所有权的智能指针它保证同一时间只有一个 unique_ptr 指向某个资源当 unique_ptr 超出作用域或被销毁时它会自动释放所指向的资源。 unique_ptr 不支持拷贝和赋值操作但是可以通过 std::move 转移所有权给另一个 unique_ptr。 unique_ptr 可以指向单个对象或者数组并且可以自定义删除器来释放资源。std::shared_ptr 是一种共享所有权的智能指针它允许多个 shared_ptr 指向同一个资源并且维护一个引用计数来记录有多少个 shared_ptr 共享该资源。 当最后一个 shared_ptr 超出作用域或被销毁时它会自动释放所指向的资源。shared_ptr 支持拷贝和赋值操作每次拷贝或赋值都会增加引用计数。shared_ptr 也可以自定义删除器来释放资源。std::weak_ptr 是一种弱引用的智能指针它不会增加所指向资源的引用计数也不会影响资源的生命周期。weak_ptr 只能通过 shared_ptr 来构造它可以观察 shared_ptr 所管理的资源但是不能直接访问。为了访问资源需要先调用 lock 方法将 weak_ptr 转换为 shared_ptr。 weak_ptr 的作用是防止 shared_ptr 之间的循环引用导致内存泄漏的问题。
指针和引用
指针和引用都是 C 中表示内存地址的概念但是它们有很多不同的特点和用法。
定义和性质指针是一个变量它存储的是一个地址指向内存中的一个存储单元引用是一个变量的别名它和原变量实质上是同一个东西占用同一个存储单元。
例如
int a 10; // 定义一个整型变量 a
int *p a; // 定义一个指针 p它的值是 a 的地址
int r a; // 定义一个引用 r它是 a 的别名初始化指针可以在定义时不初始化或者初始化为 NULL引用必须在定义时初始化并且不能为 NULL。例如
int *p; // 合法p 是一个未初始化的指针
int r; // 不合法r 必须初始化
int *q NULL; // 合法q 是一个空指针
int s NULL; // 不合法s 不能为 NULL修改指针可以在初始化后改变它所指向的对象引用一旦初始化后就不能再改变它所引用的对象。 例如
int a 10;
int b 20;
int *p a; // p 指向 a
int r a; // r 引用 a
p b; // 合法p 改变为指向 b
r b; // 不合法r 不能改变为引用 b这里相当于给 a 赋值为 b操作指针需要通过解引用操作符*来访问或者修改所指对象的值引用可以直接访问或者修改所引用对象的值。 例如
int a 10;
int *p a;
int r a;
*p 20; // 合法通过解引用修改 p 所指对象的值为 20
r 30; // 合法直接修改 r 所引用对象的值为 30多级指针可以有多级即指向指针的指针引用只能有一级即不能有引用的引用。¹² 例如
int a 10;
int *p a;
int **q p; // 合法q 是一个二级指针指向 p
int r a;
int s r; // 不合法s 不能是 r 的引用sizeof指针的 sizeof 结果是指针本身所占的字节数与所指对象的类型无关引用的 sizeof 结果是所引用对象所占的字节数与引用本身无关。 例如
int a 10;
double b 3.14;
char c A;
int *p1 a;
double *p2 b;
char *p3 c;
int r1 a;
double r2 b;
char r3 c;
cout sizeof(p1) endl; // 输出 8在64位机器上表示 p1 占8个字节
cout sizeof(p2) endl; // 输出 8在64位机器上表示 p2 占8个字节
cout sizeof(p3) endl; // 输出 8在64位机器上表示 p3 占8个字节
cout sizeof(r1) endl; // 输出 4表示 r1 所引用的对象 a 占4个字节
cout sizeof(r2) endl; // 输出 8表示 r2 所引用的对象 b 占8个字节
cout sizeof(r3) endl; // 输出 1表示 r3 所引用的对象 c 占1个字节自增指针的自增操作会根据所指对象的类型来移动指针的位置引用的自增操作相当于对所引用对象进行自增操作。 例如
int a[3] {10, 20, 30};
int *p a; // p 指向数组的第一个元素
int r a[0]; // r 引用数组的第一个元素
p; // 合法p 移动到数组的第二个元素
r; // 合法r 所引用的元素值增加 1即 a[0] 变为 11函数参数指针和引用都可以作为函数的参数从而实现对实参的修改但是指针作为参数时需要检查是否为空而引用不需要。例如
void swap1(int *a, int *b) { // 使用指针作为参数if (a NULL || b NULL) return; // 需要检查指针是否为空int temp *a;*a *b;*b temp;
}void swap2(int a, int b) { // 使用引用作为参数// 不需要检查引用是否为空因为引用不能为空int temp a;a b;b temp;
}一些常见STL
vectorvector 是一个动态数组它可以在运行时改变大小支持随机访问也就是可以通过下标来访问任意位置的元素。 vector 的头文件是 vector它的定义如下
template class T, class Allocator allocatorT
class vector;其中T 是元素的类型Allocator 是分配器的类型默认为 std::allocator。
vector 的特性有
vector 会在内存中连续存储元素因此可以高效地访问和遍历元素。vector 可以在尾部快速地插入和删除元素但是在中间或者头部插入或删除元素会导致后面的元素移动效率较低。vector 会根据需要自动调整容量当容量不足时会重新分配更大的内存空间并复制原来的元素。vector 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
vector 的用法有
可以使用构造函数来创建一个 vector 对象并指定初始大小、值、容量等参数。
vectorint v1; // 创建一个空的 vector
vectorint v2(10); // 创建一个包含 10 个 int 类型元素的 vector默认值为 0
vectorint v3(10, 1); // 创建一个包含 10 个 int 类型元素的 vector初始值为 1
vectorint v4(v3); // 创建一个和 v3 相同的 vector
vectorint v5(v3.begin(), v3.end()); // 创建一个包含 v3 中所有元素的 vector
vectorint v6{1, 2, 3, 4, 5}; // 创建一个包含初始化列表中元素的 vector可以使用 push_back 和 pop_back 方法在尾部插入或删除元素。
v1.push_back(10); // 在 v1 的尾部插入一个值为 10 的元素
v1.pop_back(); // 删除 v1 的尾部元素可以使用 insert 和 erase 方法在任意位置插入或删除一个或多个元素。
v1.insert(v1.begin(), 20); // 在 v1 的头部插入一个值为 20 的元素
v1.insert(v1.begin() 2, 3, 30); // 在 v1 的第三个位置插入三个值为 30 的元素
v1.erase(v1.begin()); // 删除 v1 的头部元素
v1.erase(v1.begin(), v1.begin() 3); // 删除 v1 的前三个元素可以使用 resize 和 reserve 方法来改变 vector 的大小或容量。
v1.resize(5); // 改变 v1 的大小为 5如果原来小于 5则在尾部添加默认值如果原来大于 5则删除多余的元素
v1.resize(10, -1); // 改变 v1 的大小为 10如果原来小于 10则在尾部添加值为 -1 的元素如果原来大于 10则删除多余的元素
v1.reserve(20); // 改变 v1 的容量为 20如果原来小于 20则分配更大的内存空间但不改变大小如果原来大于等于 20则不做任何操作可以使用 [] 或 at 方法来访问或修改 vector 中的元素。
cout v1[0] endl; // 输出 v1 的第一个元素不检查越界
cout v1.at(0) endl; // 输出 v1 的第一个元素如果越界则抛出异常
v1[0] 100; // 修改 v1 的第一个元素为 100不检查越界
v1.at(0) 200; // 修改 v1 的第一个元素为 200如果越界则抛出异常可以使用 front 和 back 方法来访问或修改 vector 的头部或尾部元素。
cout v1.front() endl; // 输出 v1 的头部元素
cout v1.back() endl; // 输出 v1 的尾部元素
v1.front() 300; // 修改 v1 的头部元素为 300
v1.back() 400; // 修改 v1 的尾部元素为 400可以使用 begin 和 end 方法来获取 vector 的迭代器用于遍历或操作 vector 中的元素。¹²
vectorint::iterator it; // 定义一个迭代器
for (it v1.begin(); it ! v1.end(); it) { // 遍历 v1 中的所有元素cout *it ; // 输出当前元素的值*it 10; // 将当前元素的值增加 10
}
cout endl;可以使用 empty 和 size 方法来判断 vector 是否为空或获取 vector 中的元素个数。¹²
if (v1.empty()) { // 判断 v1 是否为空cout v1 is empty endl;
} else {cout v1 is not empty endl;
}
cout v1 size: v1.size() endl; // 输出 v1 中的元素个数可以使用 clear 方法来清空 vector 中的所有元素。
v1.clear(); // 清空 v1 中的所有元素setset 是一个有序的容器其中的元素按照一定的规则进行排序。它不允许重复的元素并且可以快速地插入、删除和查找元素。 set 的头文件是 set它的定义如下
template class T, class Compare lessT, class Allocator allocatorT
class set;其中T 是元素的类型Compare 是比较函数对象的类型默认为 std::less表示按照升序排序Allocator 是分配器的类型默认为 std::allocator。
set 的特性有
set 底层是用红黑树实现的因此可以保证插入、删除和查找操作的时间复杂度都是 O(log n)。set 中的元素是只读的不能通过迭代器来修改它们的值否则会破坏 set 的有序性。set 中不存在重复的元素如果插入一个已经存在的元素则会被忽略。set 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
set 的用法有
可以使用构造函数来创建一个 set 对象并指定初始元素、比较函数等参数。
setint s1; // 创建一个空的 set
setint s2{1, 2, 3, 4, 5}; // 创建一个包含初始化列表中元素的 set
setint s3(s2); // 创建一个和 s2 相同的 set
setint s4(s2.begin(), s2.end()); // 创建一个包含 s2 中所有元素的 set
setint, greaterint s5; // 创建一个按照降序排序的 set可以使用 insert 方法在 set 中插入一个或多个元素。
s1.insert(10); // 在 s1 中插入一个值为 10 的元素
s1.insert({20, 30, 40}); // 在 s1 中插入多个值为 20, 30, 40 的元素
s1.insert(s2.begin(), s2.end()); // 在 s1 中插入 s2 中的所有元素可以使用 erase 方法在 set 中删除一个或多个元素。
s1.erase(10); // 在 s1 中删除值为 10 的元素
s1.erase(s1.begin()); // 在 s1 中删除第一个元素
s1.erase(s1.begin(), s1.end()); // 在 s1 中删除所有元素可以使用 find 方法在 set 中查找一个元素返回一个指向该元素的迭代器如果没有找到则返回 end()。
auto it s1.find(10); // 在 s1 中查找值为 10 的元素
if (it ! s1.end()) {cout Found *it endl; // 输出 Found 10
} else {cout Not found endl;
}可以使用 count 方法在 set 中统计一个元素出现的次数返回一个整数值由于 set 不允许重复元素所以结果只能是 0 或 1。
int n s1.count(10); // 在 s1 中统计值为 10 的元素出现的次数
cout n endl; // 输出 0 或 1可以使用 lower_bound 和 upper_bound 方法在有序的 set 中查找第一个大于等于或大于目标值的元素的位置返回一个迭代器。
auto it1 s1.lower_bound(15); // 在 s1 中查找第一个大于等于 15 的元素的位置
auto it2 s1.upper_bound(15); // 在 s1 中查找第一个大于 15 的元素的位置
if (it1 ! s1.end()) {cout *it1 endl; // 输出找到的元素的值
} else {cout Not found endl;
}
if (it2 ! s1.end()) {cout *it2 endl; // 输出找到的元素的值
} else {cout Not found endl;
}可以使用 begin 和 end 方法来获取 set 的迭代器用于遍历或操作 set 中的元素。
setint::iterator it; // 定义一个迭代器
for (it s1.begin(); it ! s1.end(); it) { // 遍历 s1 中的所有元素cout *it ; // 输出当前元素的值
}
cout endl;可以使用 empty 和 size 方法来判断 set 是否为空或获取 set 中的元素个数。
if (s1.empty()) { // 判断 s1 是否为空cout s1 is empty endl;
} else {cout s1 is not empty endl;
}
cout s1 size: s1.size() endl; // 输出 s1 中的元素个数可以使用 clear 方法来清空 set 中的所有元素。
s1.clear(); // 清空 s1 中的所有元素mapmap 是一个关联容器它存储了键值对key-value pair类型的元素其中键是唯一的值可以重复。它可以根据键快速地插入、删除和查找对应的值。 map 的头文件是 map它的定义如下
template class Key, class T, class Compare lessKey, class Allocator allocatorpairconst Key, T
class map;其中Key 是键的类型T 是值的类型Compare 是比较函数对象的类型默认为 std::less表示按照升序排序Allocator 是分配器的类型默认为 std::allocatorpairconst Key, T。
map 的特性有
map 底层是用红黑树实现的因此可以保证插入、删除和查找操作的时间复杂度都是 O(log n)。map 中的键是只读的不能通过迭代器来修改它们的值否则会破坏 map 的有序性。map 中不存在重复的键如果插入一个已经存在的键则会覆盖原来的值。map 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
map 的用法有
可以使用构造函数来创建一个 map 对象并指定初始元素、比较函数等参数。
mapint, string m1; // 创建一个空的 map
mapint, string m2{{1, one}, {2, two}, {3, three}}; // 创建一个包含初始化列表中元素的 map
mapint, string m3(m2); // 创建一个和 m2 相同的 map
mapint, string m4(m2.begin(), m2.end()); // 创建一个包含 m2 中所有元素的 map
mapint, string, greaterint m5; // 创建一个按照降序排序的 map可以使用 insert 方法在 map 中插入一个或多个键值对。
m1.insert({10, ten}); // 在 m1 中插入一个键值对 {10, ten}
m1.insert({{20, twenty}, {30, thirty}}); // 在 m1 中插入多个键值对 {{20, twenty}, {30, thirty}}
m1.insert(m2.begin(), m2.end()); // 在 m1 中插入 m2 中的所有键值对可以使用 erase 方法在 map 中删除一个或多个键值对。
m1.erase(10); // 在 m1 中删除键为 10 的键值对
m1.erase(m1.begin()); // 在 m1 中删除第一个键值对
m1.erase(m1.begin(), m1.end()); // 在 m1 中删除所有键值对可以使用 find 方法在 map 中查找一个键对应的值返回一个指向该键值对的迭代器如果没有找到则返回 end()。
auto it m1.find(10); // 在 m1 中查找键为 10 的键值对
if (it ! m1.end()) {cout Found it-first : it-second endl; // 输出 Found 10: ten
} else {cout Not found endl;
}可以使用 count 方法在 map 中统计一个键出现的次数返回一个整数值由于 map 不允许重复键所以结果只能是 0 或 1。
int n m1.count(10); // 在 m1 中统计键为 10 的键值对出现的次数
cout n endl; // 输出 0 或 1可以使用 lower_bound 和 upper_bound 方法在有序的 map 中查找第一个大于等于或大于目标键的键值对的位置返回一个迭代器。
auto it1 m1.lower_bound(15); // 在 m1 中查找第一个大于等于 15 的键值对的位置
auto it2 m1.upper_bound(15); // 在 m1 中查找第一个大于 15 的键值对的位置
if (it1 ! m1.end()) {cout it1-first : it1-second endl; // 输出找到的键值对
} else {cout Not found endl;
}
if (it2 ! m1.end()) {cout it2-first : it2-second endl; // 输出找到的键值对
} else {cout Not found endl;
}可以使用 begin 和 end 方法来获取 map 的迭代器用于遍历或操作 map 中的键值对。
mapint, string::iterator it; // 定义一个迭代器
for (it m1.begin(); it ! m1.end(); it) { // 遍历 m1 中的所有键值对cout it-first : it-second ; // 输出当前键值对
}
cout endl;可以使用 empty 和 size 方法来判断 map 是否为空或获取 map 中的键值对个数。
if (m1.empty()) { // 判断 m1 是否为空cout m1 is empty endl;
} else {cout m1 is not empty endl;
}
cout m1 size: m1.size() endl; // 输出 m1 中的键值对个数可以使用 clear 方法来清空 map 中的所有键值对。
m1.clear(); // 清空 m1 中的所有键值对dequedeque 是一个双端队列它可以在头部和尾部快速地插入和删除元素也支持随机访问但是相比 vector它的随机访问效率较低。 deque 的头文件是 deque它的定义如下
template class T, class Allocator allocatorT
class deque;其中T 是元素的类型Allocator 是分配器的类型默认为 std::allocator。
deque 的特性有
deque 不是在内存中连续存储元素而是分成多个块每个块连续存储一部分元素然后用一个索引表来记录每个块的位置。deque 可以在头部和尾部快速地插入和删除元素不会影响其他元素的位置但是在中间插入或删除元素会导致后面的元素移动效率较低。deque 支持随机访问可以通过下标来访问任意位置的元素但是由于需要先查找块的位置再访问元素所以效率不如 vector 高。deque 不会自动调整容量当容量不足时会在头部或尾部分配新的块并更新索引表。deque 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
deque 的用法有
可以使用构造函数来创建一个 deque 对象并指定初始大小、值等参数。
dequeint d1; // 创建一个空的 deque
dequeint d2(10); // 创建一个包含 10 个 int 类型元素的 deque默认值为 0
dequeint d3(10, 1); // 创建一个包含 10 个 int 类型元素的 deque初始值为 1
dequeint d4(d3); // 创建一个和 d3 相同的 deque
dequeint d5(d3.begin(), d3.end()); // 创建一个包含 d3 中所有元素的 deque
dequeint d6{1, 2, 3, 4, 5}; // 创建一个包含初始化列表中元素的 deque可以使用 push_front 和 push_back 方法在 deque 的头部或尾部插入元素。
d1.push_front(10); // 在 d1 的头部插入一个值为 10 的元素
d1.push_back(20); // 在 d1 的尾部插入一个值为 20 的元素可以使用 pop_front 和 pop_back 方法在 deque 的头部或尾部删除元素。
d1.pop_front(); // 删除 d1 的头部元素
d1.pop_back(); // 删除 d1 的尾部元素可以使用 insert 和 erase 方法在任意位置插入或删除一个或多个元素。
d1.insert(d1.begin(), 30); // 在 d1 的头部插入一个值为 30 的元素
d1.insert(d1.begin() 2, 3, 40); // 在 d1 的第三个位置插入三个值为 40 的元素
d1.erase(d1.begin()); // 删除 d1 的头部元素
d1.erase(d1.begin(), d1.begin() 3); // 删除 d1 的前三个元素可以使用 resize 方法来改变 deque 的大小。
d1.resize(5); // 改变 d1 的大小为 5如果原来小于 5则在尾部添加默认值如果原来大于 5则删除多余的元素
d1.resize(10, -1); // 改变 d1 的大小为 10如果原来小于 10则在尾部添加值为 -1 的元素如果原来大于 10则删除多余的元素可以使用 [] 或 at 方法来访问或修改 deque 中的元素。
cout d1[0] endl; // 输出 d1 的第一个元素不检查越界
cout d1.at(0) endl; // 输出 d1 的第一个元素如果越界则抛出异常
d1[0] 100; // 修改 d1 的第一个元素为 100不检查越界
d1.at(0) 200; // 修改 d1 的第一个元素为 200如果越界则抛出异常可以使用 front 和 back 方法来访问或修改 deque 的头部或尾部元素。
cout d1.front() endl; // 输出 d1 的头部元素
cout d1.back() endl; // 输出 d1 的尾部元素
d1.front() 300; // 修改 d1 的头部元素为 300
d1.back() 400; // 修改 d1 的尾部元素为 400可以使用 begin 和 end 方法来获取 deque 的迭代器用于遍历或操作 deque 中的元素。
dequeint::iterator it; // 定义一个迭代器
for (it d1.begin(); it ! d1.end(); it) { // 遍历 d1 中的所有元素cout *it ; // 输出当前元素的值*it 10; // 将当前元素的值增加 10
}
cout endl;可以使用 empty 和 size 方法来判断 deque 是否为空或获取 deque 中的元素个数。
if (d1.empty()) { // 判断 d1 是否为空cout d1 is empty endl;
} else {cout d1 is not empty endl;
}
cout d1 size: d1.size() endl; // 输出 d1 中的元素个数可以使用 clear 方法来清空 deque 中的所有元素。
d1.clear(); // 清空 d1 中的所有元素queuequeue 是一个先进先出FIFO的队列它只允许在尾部插入元素在头部删除元素。 queue 的头文件是 queue它的定义如下
template class T, class Container dequeT
class queue;其中T 是元素的类型Container 是底层容器的类型默认为 std::deque。
queue 的特性有
queue 是一个适配器它不是直接实现数据结构和算法而是利用另一个容器如 deque来提供基本功能并在此基础上添加一些限制和接口。queue 只提供了有限的操作只能在尾部插入元素在头部删除或访问元素不能随机访问或修改其他位置的元素。queue 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
queue 的用法有
可以使用构造函数来创建一个 queue 对象并指定初始容器等参数。
queueint q1; // 创建一个空的 queue
dequeint d{1, 2, 3, 4, 5}; // 创建一个 deque 对象
queueint q2(d); // 创建一个包含 deque 中所有元素的 queue可以使用 push 方法在 queue 的尾部插入一个元素。
q1.push(10); // 在 q1 的尾部插入一个值为 10 的元素可以使用 pop 方法在 queue 的头部删除一个元素。
q1.pop(); // 删除 q1 的头部元素可以使用 front 和 back 方法来访问 queue 的头部或尾部元素。
cout q1.front() endl; // 输出 q1 的头部元素
cout q1.back() endl; // 输出 q1 的尾部元素可以使用 empty 和 size 方法来判断 queue 是否为空或获取 queue 中的元素个数。
if (q1.empty()) { // 判断 q1 是否为空cout q1 is empty endl;
} else {cout q1 is not empty endl;
}
cout q1 size: q1.size() endl; // 输出 q1 中的元素个数unordered_setunordered_set 是一个无序的容器它存储了不重复的元素并且可以快速地插入、删除和查找元素。 unordered_set 的头文件是 unordered_set它的定义如下
template class T, class Hash hashT, class KeyEqual equal_toT, class Allocator allocatorT
class unordered_set;其中T 是元素的类型Hash 是哈希函数对象的类型默认为 std::hashKeyEqual 是判断两个元素是否相等的函数对象的类型默认为 std::equal_toAllocator 是分配器的类型默认为 std::allocator。
unordered_set 的特性有
unordered_set 底层是用哈希表实现的因此可以保证插入、删除和查找操作的平均时间复杂度都是 O(1)。unordered_set 中的元素是无序的不能通过下标或迭代器来访问或修改它们的顺序。unordered_set 中不存在重复的元素如果插入一个已经存在的元素则会被忽略。unordered_set 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
unordered_set 的用法有
可以使用构造函数来创建一个 unordered_set 对象并指定初始元素、哈希函数、相等函数等参数。
unordered_setint s1; // 创建一个空的 unordered_set
unordered_setint s2{1, 2, 3, 4, 5}; // 创建一个包含初始化列表中元素的 unordered_set
unordered_setint s3(s2); // 创建一个和 s2 相同的 unordered_set
unordered_setint s4(s2.begin(), s2.end()); // 创建一个包含 s2 中所有元素的 unordered_set可以使用 insert 方法在 unordered_set 中插入一个或多个元素。
s1.insert(10); // 在 s1 中插入一个值为 10 的元素
s1.insert({20, 30, 40}); // 在 s1 中插入多个值为 20, 30, 40 的元素
s1.insert(s2.begin(), s2.end()); // 在 s1 中插入 s2 中的所有元素可以使用 erase 方法在 unordered_set 中删除一个或多个元素。
s1.erase(10); // 在 s1 中删除值为 10 的元素
s1.erase(s1.begin()); // 在 s1 中删除第一个元素
s1.erase(s1.begin(), s1.end()); // 在 s1 中删除所有元素可以使用 find 方法在 unordered_set 中查找一个元素返回一个指向该元素的迭代器如果没有找到则返回 end()。
auto it s1.find(10); // 在 s1 中查找值为 10 的元素
if (it ! s1.end()) {cout Found *it endl; // 输出 Found 10
} else {cout Not found endl;
}可以使用 count 方法在 unordered_set 中统计一个元素出现的次数返回一个整数值由于 unordered_set 不允许重复元素所以结果只能是 0 或 1。
int n s1.count(10); // 在 s1 中统计值为 10 的元素出现的次数
cout n endl; // 输出 0 或 1可以使用 begin 和 end 方法来获取 unordered_set 的迭代器用于遍历或操作 unordered_set 中的元素。
unordered_setint::iterator it; // 定义一个迭代器
for (it s1.begin(); it ! s1.end(); it) { // 遍历 s1 中的所有元素cout *it ; // 输出当前元素的值
}
cout endl;可以使用 empty 和 size 方法来判断 unordered_set 是否为空或获取 unordered_set 中的元素个数。
if (s1.empty()) { // 判断 s1 是否为空cout s1 is empty endl;
} else {cout s1 is not empty endl;
}
cout s1 size: s1.size() endl; // 输出 s1 中的元素个数可以使用 clear 方法来清空 unordered_set 中的所有元素。
s1.clear(); // 清空 s1 中的所有元素unordered_mapunordered_map 是一个关联容器它存储了键值对key-value pair类型的元素其中键是唯一的值可以重复。它可以根据键快速地插入、删除和查找对应的值。 unordered_map 的头文件是 unordered_map它的定义如下
template class Key, class T, class Hash hashKey, class KeyEqual equal_toKey, class Allocator allocatorpairconst Key, T
class unordered_map;其中Key 是键的类型T 是值的类型Hash 是哈希函数对象的类型默认为 std::hashKeyEqual 是判断两个键是否相等的函数对象的类型默认为 std::equal_toAllocator 是分配器的类型默认为 std::allocatorpairconst Key, T。
unordered_map 的特性有
unordered_map 底层是用哈希表实现的因此可以保证插入、删除和查找操作的平均时间复杂度都是 O(1)。unordered_map 中的键是无序的不能通过下标或迭代器来访问或修改它们的顺序。unordered_map 中不存在重复的键如果插入一个已经存在的键则会覆盖原来的值。unordered_map 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
unordered_map 的用法有
可以使用构造函数来创建一个 unordered_map 对象并指定初始元素、哈希函数、相等函数等参数。
unordered_mapint, string m1; // 创建一个空的 unordered_map
unordered_mapint, string m2{{1, one}, {2, two}, {3, three}}; // 创建一个包含初始化列表中元素的 unordered_map
unordered_mapint, string m3(m2); // 创建一个和 m2 相同的 unordered_map
unordered_mapint, string m4(m2.begin(), m2.end()); // 创建一个包含 m2 中所有元素的 unordered_map可以使用 insert 方法在 unordered_map 中插入一个或多个键值对。
m1.insert({10, ten}); // 在 m1 中插入一个键值对 {10, ten}
m1.insert({{20, twenty}, {30, thirty}}); // 在 m1 中插入多个键值对 {{20, twenty}, {30, thirty}}
m1.insert(m2.begin(), m2.end()); // 在 m1 中插入 m2 中的所有键值对可以使用 erase 方法在 unordered_map 中删除一个或多个键值对。
m1.erase(10); // 在 m1 中删除键为 10 的键值对
m1.erase(m1.begin()); // 在 m1 中删除第一个键值对
m1.erase(m1.begin(), m1.end()); // 在 m1 中删除所有键值对可以使用 find 方法在 unordered_map 中查找一个键对应的值返回一个指向该键值对的迭代器如果没有找到则返回 end()。
auto it m1.find(10); // 在 m1 中查找键为 10 的键值对
if (it ! m1.end()) {cout Found it-first : it-second endl; // 输出 Found 10: ten
} else {cout Not found endl;
}可以使用 count 方法在 unordered_map 中统计一个键出现的次数返回一个整数值由于 unordered_map 不允许重复键所以结果只能是 0 或 1。
int n m1.count(10); // 在 m1 中统计键为 10 的键值对出现的次数
cout n endl; // 输出 0 或 1可以使用 begin 和 end 方法来获取 unordered_map 的迭代器用于遍历或操作 unordered_map 中的键值对。
unordered_mapint, string::iterator it; // 定义一个迭代器
for (it m1.begin(); it ! m1.end(); it) { // 遍历 m1 中的所有键值对cout it-first : it-second ; // 输出当前键值对
}
cout endl;可以使用 empty 和 size 方法来判断 unordered_map 是否为空或获取 unordered_map 中的键值对个数。
if (m1.empty()) { // 判断 m1 是否为空cout m1 is empty endl;
} else {cout m1 is not empty endl;
}
cout m1 size: m1.size() endl; // 输出 m1 中的键值对个数可以使用 clear 方法来清空 unordered_map 中的所有键值对。
m1.clear(); // 清空 m1 中的所有键值对stackstack 是一个后进先出LIFO的栈它只允许在顶部插入和删除元素。 stack 的头文件是 stack它的定义如下
template class T, class Container dequeT
class stack;其中T 是元素的类型Container 是底层容器的类型默认为 std::deque。
stack 的特性有
stack 是一个适配器它不是直接实现数据结构和算法而是利用另一个容器如 deque来提供基本功能并在此基础上添加一些限制和接口。stack 只提供了有限的操作只能在顶部插入、删除或访问元素不能随机访问或修改其他位置的元素。stack 支持拷贝、赋值、移动等操作可以方便地复制或转移数据。
stack 的用法有
可以使用构造函数来创建一个 stack 对象并指定初始容器等参数。
stackint s1; // 创建一个空的 stack
dequeint d{1, 2, 3, 4, 5}; // 创建一个 deque 对象
stackint s2(d); // 创建一个包含 deque 中所有元素的 stack可以使用 push 方法在 stack 的顶部插入一个元素。
s1.push(10); // 在 s1 的顶部插入一个值为 10 的元素可以使用 pop 方法在 stack 的顶部删除一个元素。
s1.pop(); // 删除 s1 的顶部元素可以使用 top 方法来访问或修改 stack 的顶部元素。
cout s1.top() endl; // 输出 s1 的顶部元素
s1.top() 20; // 修改 s1 的顶部元素为 20可以使用 empty 和 size 方法来判断 stack 是否为空或获取 stack 中的元素个数。
if (s1.empty()) { // 判断 s1 是否为空cout s1 is empty endl;
} else {cout s1 is not empty endl;
}
cout s1 size: s1.size() endl; // 输出 s1 中的元素个数多态多态是指同一个接口可以有不同的实现方式从而实现不同的功能。多态是面向对象编程的核心概念它可以提高代码的复用性和扩展性。静态多态静态多态是指在编译期就确定了调用的函数它是通过重载和模板技术实现的。静态多态不需要虚函数也不需要继承关系只需要各个具体类的实现中要求相同的接口声明这里的接口称之为隐式接口。静态多态的优点是效率高缺点是灵活性低。动态多态动态多态是指在运行期才确定调用的函数它是通过虚函数和继承关系来实现的。动态多态需要抽象出相关类之间共同的功能集合在基类中将共同的功能声明为多个公共虚函数接口子类通过重写这些虚函数实现各自对应的功能。在调用时通过基类的指针或引用来操作这些子类对象所需执行的虚函数会自动绑定到对应的子类对象上去。动态多态的优点是灵活性高缺点是效率低。
动态多态的实现原理如下
当一个类中有虚函数时编译器会为该类生成一个虚函数表vtable存储该类中所有虚函数的地址。同时编译器会为该类的每个对象添加一个虚指针vptr指向该类的虚函数表。当一个子类继承了一个基类时子类也会继承基类的虚函数表并且如果子类重写了某个虚函数那么子类的虚函数表中对应位置的地址会被替换为子类自己的虚函数地址。当通过基类指针或引用调用一个虚函数时编译器会根据该指针或引用所指向的对象的类型找到对应的虚函数表并根据虚函数在表中的偏移量找到正确的虚函数地址并调用之。
下面是一个简单的例子
// 基类
class Animal {
public:// 虚析构函数virtual ~Animal() {}// 虚函数virtual void speak() {cout Animal speak endl;}
};// 子类
class Dog : public Animal {
public:// 重写虚函数virtual void speak() {cout Dog speak endl;}
};// 子类
class Cat : public Animal {
public:// 重写虚函数virtual void speak() {cout Cat speak endl;}
};// 测试代码
int main() {// 基类指针Animal* p nullptr;// 指向子类对象p new Dog();// 调用虚函数p-speak(); // 输出 Dog speak// 释放内存delete p;// 指向另一个子类对象p new Cat();// 调用虚函数p-speak(); // 输出 Cat speak// 释放内存delete p;return 0;
}不能是虚函数的函数 虚函数虚函数是一种特殊的成员函数它可以在基类中声明为 virtual并在派生类中重写从而实现动态多态。虚函数的调用是通过虚函数表vtable和虚指针vptr来实现的每个有虚函数的类都有一个虚函数表存储该类的所有虚函数的地址每个有虚函数的对象都有一个虚指针指向该对象所属类的虚函数表。当通过基类指针或引用调用一个虚函数时会根据该指针或引用所指向的对象的类型找到对应的虚函数表并根据虚函数在表中的偏移量找到正确的虚函数地址并调用之。 不能是虚函数的函数C 中有一些特殊的成员函数它们不能被声明为虚函数主要有以下几种 构造函数构造函数是用于创建对象并初始化对象状态的成员函数它不能被声明为虚函数因为在创建对象时还没有分配内存空间也没有生成虚指针和虚函数表因此无法调用虚函数。如果将构造函数声明为虚函数编译器会报错。但是构造函数可以调用其他的虚函数只是这时候调用的是自己类中定义或继承的版本而不是子类重写的版本。 析构函数析构函数是用于销毁对象并释放资源的成员函数它可以被声明为虚函数以实现多态的析构。如果一个基类指针或引用指向一个派生类对象并且通过该指针或引用来删除该对象那么如果基类的析构函数不是虚函数就只会调用基类的析构函数而不会调用派生类的析构函数从而导致资源泄漏或其他错误。但是析构函数不能被声明为纯虚函数pure virtual function也就是在声明时后面加上 0 的那种。因为纯虚函数表示没有实现需要子类来提供实现而每个类都需要有自己的析构函数来执行清理工作。如果将析构函数声明为纯虚函数编译器会报错。 静态成员函数静态成员函数是属于类本身而不属于对象的成员函数它可以通过类名或对象来调用但是它不包含 this 指针也不受访问控制符的限制。静态成员函数不能被声明为虚函数因为静态成员函数不依赖于对象的存在也就无法使用虚指针和虚函数表来实现多态。如果将静态成员函数声明为虚函数编译器会报错。 友元函数友元函数是一种特殊的非成员函数它可以访问某个类或对象的私有或保护成员但是它不属于该类或对象。友元关系不能被继承或传递。友元函数不能被声明为虚函数。 内联inline成员函数内联成员函数是一种优化技术它可以在编译期将内联成员函数的代码直接嵌入到调用处从而避免了普通成员函数调用时产生的额外开销如参数传递、栈帧分配等。内联成员函数可以在定义时加上 inline 关键字来显式声明也可以在类内部定义时隐式声明。内联成员函数不能被声明为虚函数因为虚函数的调用是通过虚函数表和虚指针来实现的这与内联成员函数的优化目的相矛盾。如果将内联成员函数声明为虚函数编译器会忽略 inline 关键字将其当作普通的虚函数处理。
vector 和 list
vectorvector 是一种顺序容器它使用连续的内存空间来存储元素类似于数组但是它可以动态地调整大小。vector 的头文件是 vector它的定义如下
template class T, class Allocator allocatorT
class vector;其中T 是元素的类型Allocator 是分配器的类型默认为 std::allocator。
vector 的特性有 vector 支持随机访问可以通过下标运算符 [] 或 at 方法来访问或修改任意位置的元素时间复杂度为 O(1)。 vector 支持在尾部插入或删除元素可以使用 push_back 或 pop_back 方法来实现时间复杂度为 O(1)摊还。 vector 不支持在头部或中间插入或删除元素如果要实现可以使用 insert 或 erase 方法但是时间复杂度为 O(n)因为需要移动后面的元素。 vector 有一个容量capacity和一个大小size的概念容量表示已经分配的内存空间能够容纳的元素个数大小表示实际存储的元素个数。当大小超过容量时vector 会重新分配一块更大的内存空间并将原来的元素复制过去这会导致效率降低和迭代器失效。可以使用 reserve 方法来预先分配足够的内存空间避免频繁的重新分配。 listlist 是一种顺序容器它使用双向链表来存储元素每个节点包含一个元素和两个指针分别指向前一个节点和后一个节点。list 的头文件是 list它的定义如下
template class T, class Allocator allocatorT
class list;其中T 是元素的类型Allocator 是分配器的类型默认为 std::allocator。
list 的特性有 list 不支持随机访问只能通过迭代器或指针来访问或修改任意位置的元素时间复杂度为 O(n)。 list 支持在头部或尾部插入或删除元素可以使用 push_front, pop_front, push_back 或 pop_back 方法来实现时间复杂度为 O(1)。 list 支持在中间插入或删除元素可以使用 insert 或 erase 方法来实现时间复杂度为 O(1)因为不需要移动其他元素。 list 没有容量和大小的概念它只有一个大小size的概念表示实际存储的元素个数。list 不需要重新分配内存空间因为它是动态地分配每个节点的内存空间。 区别和适用场景vector 和 list 容器的区别主要在于它们使用的数据结构和内存管理方式不同这导致了它们在性能和功能上有各自的优缺点。一般来说 如果需要频繁地访问或修改任意位置的元素或者需要高效地利用内存空间那么可以选择 vector 容器。如果需要频繁地在头部或中间插入或删除元素或者需要灵活地调整容器大小那么可以选择 list 容器。
虚指针
虚指针Virtual Pointer是一种特殊的指针它存在于含有虚函数的类的对象中用于指向一个虚函数表Virtual Function Table。虚函数表是一个存储了该类所有虚函数地址的数组它在编译时就被创建好了。¹²
当一个类被继承时如果子类重写了父类的虚函数那么子类就会有自己的虚函数表其中包含了子类自己的虚函数地址。如果子类没有重写父类的虚函数那么子类就会共用父类的虚函数表。²³
虚指针和虚函数表的作用是实现多态性也就是让基类指针可以根据所指向的对象的不同动态地调用相应的虚函数。这个过程发生在运行时也叫做动态绑定Dynamic Binding。¹⁴
下面是一个简单的例子演示了虚指针和虚函数表的使用
#include iostream
using namespace std;class A { // 基类A
public:virtual void f() { // 虚函数fcout A::f() endl;}
};class B : public A { // 子类B
public:void f() override { // 重写虚函数fcout B::f() endl;}
};int main() {A a; // 基类对象aB b; // 子类对象bA* p a; // 基类指针p指向ap-f(); // 调用A::f()p b; // 基类指针p指向bp-f(); // 调用B::f()return 0;
}输出结果为
A::f()
B::f()在这个例子中基类A和子类B都有一个虚指针分别指向各自的虚函数表。基类A的虚函数表中只有一个元素就是A::f()的地址。子类B的虚函数表中也只有一个元素就是B::f()的地址。当基类指针p指向不同的对象时它会通过对象中的虚指针找到对应的虚函数表并从中取出正确的虚函数地址来调用。