OOP面向对象程序设计_复习记录
为了应付明天的OOP期末考试
记录自己复习了些啥
纠了比较多的细节
顺序完全乱序
1 lambda 表达式
1.1 介绍
auto var = [capture-clause] (aotu param) -> bool
{
...
}
在C++的lambda表达式中,方括号[]
内是捕获列表(capture list),用于指定lambda表达式可以访问哪些在它之外作用域定义的变量。捕获列表可以包含以下几种内容:
-
值捕获(capture by value):当一个变量被值捕获时,lambda表达式会获取该变量当前值的一份拷贝。例如,
[x]
就是值捕获变量x。 -
引用捕获(capture by reference):当一个变量被引用捕获时,lambda表达式会持有一个到该变量的引用。这意味着lambda表达式内部可以改变被捕获变量的值。例如,
[&x]
就是引用捕获变量x。 -
隐式值捕获:
[=]
表示值捕获所有父作用域中的变量。 -
隐式引用捕获:
[&]
表示引用捕获所有父作用域中的变量。 -
混合捕获:你可以混合使用值捕获和引用捕获,例如
[x, &y]
表示值捕获x,引用捕获y。 -
this指针捕获:你可以通过
[=, this]
或者[this]
捕获当前对象的this指针,以便在lambda表达式中访问类的成员变量和成员函数。 -
初始化捕获(C++14及以后版本):你可以在捕获列表中声明并初始化一个新的变量,然后在lambda表达式中使用它,例如
[y = x * 2]
。
注意:默认情况下,被值捕获的变量在lambda表达式中是只读的,不能被修改。如果你需要在lambda表达式中修改一个被值捕获的变量,可以添加mutable
关键字。例如:
int x = 10;
auto f = [x]() mutable {
x = 20; // OK,因为lambda表达式是mutable的
};
当你通过引用捕获变量时,你可以在lambda表达式内修改该变量的值,无需mutable
关键字。
int x = 10;
auto f = [&x]() {
x = 20; // OK,因为x是通过引用捕获的
};
在C++的lambda表达式中,你可以使用this
关键字来捕获当前对象的this指针。这允许你在lambda表达式中访问当前对象的成员变量和成员函数。
例如,考虑以下类的定义:
class MyClass {
public:
int value = 10;
void printValue() {
auto lambda = [this]() {
std::cout << "Value: " << value << std::endl; // 直接访问成员变量
};
lambda();
}
};
在这个例子中,我们在成员函数printValue
中定义了一个lambda表达式,并在这个lambda表达式中使用了this
指针来访问成员变量value
。因为我们捕获了this
指针,所以我们可以在lambda表达式中直接访问value
,就好像我们在类的其他成员函数中一样。
然后我们可以这样使用它:
MyClass obj;
obj.printValue(); // 输出 "Value: 10"
1.2 用法
C++中的lambda表达式有很多用途。下面列举了一些常见的例子:
-
排序函数:在使用如
std::sort
这样的STL函数时,你可以使用lambda表达式来提供自定义的比较函数。std::vector<int> numbers = {1, 5, 3, 4, 2}; std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; // 降序排序 });
-
线程函数:如果你需要创建一个线程,并且线程函数非常简短,你可以使用lambda表达式。
std::thread t([]() { std::cout << "Hello, thread!" << std::endl; }); t.join();
-
捕获外部变量:你可以使用lambda表达式来捕获外部作用域中的变量。
int x = 10; auto add_x = [x](int a) { return a + x; }; std::cout << add_x(5); // 输出 15
-
在算法中使用:很多STL算法接受函数作为参数。在这种情况下,lambda表达式往往是一个很好的选择。
std::vector<int> numbers = {1, 2, 3, 4, 5}; int count = std::count_if(numbers.begin(), numbers.end(), [](int a) { return a % 2 == 0; // 统计偶数的数量 }); std::cout << count; // 输出 2
-
用作回调函数:在需要提供回调函数的情况下,可以使用lambda表达式,尤其是当回调逻辑比较简单的时候。
-
mutable lambda:如果你需要修改捕获的值,你可以使用mutable关键字。
int x = 5; auto f = [x]() mutable { x = 10; std::cout << x << std::endl; // 输出 10 }; f(); std::cout << x << std::endl; // 输出 5
在这个例子中,尽管在lambda表达式中修改了x的值,但这个修改不会影响到外部的x变量,因为lambda表达式是通过值捕获的x。
这些就是C++中lambda表达式的一些常见用法。需要注意的是,lambda表达式非常灵活,你可以在很多不同的情况下使用它们。
2 throw & thr & catch
2.1 介绍
在C++中,异常处理机制是通过try
、catch
和throw
关键字来实现的,用于处理在程序运行过程中出现的特殊情况。
这是一个基本的异常处理结构:
try {
// 需要被保护的代码块,可能会抛出异常
throw expression; // 当满足某些条件时,抛出异常
}
catch (type1 arg1) {
// 当try块中抛出的异常类型匹配type1时,执行此代码块
}
catch (type2 arg2) {
// 当try块中抛出的异常类型匹配type2时,执行此代码块
}
// 你可以有任意多个catch块来处理不同类型的异常
catch (...) {
// 当try块中抛出的异常类型与前面所有catch块都不匹配时,执行此代码块
}
这里是一些具体的说明:
-
try
:try
关键字后面跟随一段可能会抛出异常的代码块。 -
throw
:throw
关键字用于在发生某些特定情况时抛出一个异常。你可以抛出任意类型的对象(例如,一个内置类型的值、一个字符串、一个对象等)。 -
catch
:catch
关键字后面跟随一个异常类型和一个参数,用于捕获对应类型的异常。当在try
块中抛出一个异常时,会查找匹配的catch
块来处理该异常。如果找到一个匹配的catch
块,则执行该catch
块中的代码。
这是一个简单的例子:
#include <iostream>
#include <string>
int main() {
try {
throw std::string("An error occurred!"); // 抛出一个异常
}
catch (std::string e) { // 捕获类型为std::string的异常
std::cout << "Caught an exception: " << e << std::endl;
}
return 0;
}
在这个例子中,我们在try
块中抛出一个std::string
类型的异常,并在catch
块中捕获并处理这个异常。
需要注意的是,当一个异常被抛出但没有被捕获(也就是说,没有匹配的catch
块)时,程序会调用std::terminate()
函数,并且通常会导致程序异常终止。为了防止这种情况,你可以提供一个捕获所有类型异常的catch
块,它通常被写作catch (...)
。
最后,请注意,尽管异常处理是一种处理运行时错误的有用工具,但它也可能导致一些性能开销。因此,你应该只在需要的时候使用它,并且尽可能地使其与正常控制流分开。
2.2 一些例子
![image-20230612205922725](https://s1.imagehub.cc/images/2023/06/13/image-20230612205922725.png)
以下是一些 noexcept
关键字的用法示例:
- 函数声明中的
noexcept
:
void myFunction() noexcept;
上述声明表示函数 myFunction
不会抛出异常。
- 函数定义中的
noexcept
:
void myFunction() noexcept {
// 函数体
}
在函数定义中使用 noexcept
关键字,表示函数保证不会抛出异常。
- 条件性的
noexcept
:
void myFunction() noexcept(condition);
条件性的 noexcept
允许在特定条件下指定函数是否抛出异常。condition
可以是一个表达式,它的结果将决定函数是否被视为 noexcept
。
3 智能指针
3.1 介绍
在C++中,智能指针是一种对象,它可以像常规指针那样存储和操作内存地址,但它们有一个额外的特性:它们负责自动管理所指向的内存,使得开发者无需手动分配和释放内存。当智能指针不再需要其所指向的对象时(例如,当智能指针离开其作用域或被重新赋值时),它会自动删除(析构和释放)所指向的对象。
在C++标准库中,包含了以下几种类型的智能指针:
-
std::unique_ptr
: 这种智能指针保证同一时间内只有一个智能指针可以指向同一个对象。你不能复制std::unique_ptr
,但你可以移动它(即,将所有权从一个智能指针转移给另一个智能指针)。当std::unique_ptr
被销毁(例如,它离开了其作用域)时,它会删除其所指向的对象。 -
std::shared_ptr
: 这种智能指针允许多个智能指针指向同一个对象。std::shared_ptr
使用引用计数来跟踪有多少个智能指针共享同一个对象。每当创建一个新的std::shared_ptr
或者一个std::shared_ptr
被删除时,这个计数会被更新。当这个计数变为0(即,没有智能指针指向该对象)时,对象会被删除。 -
std::weak_ptr
: 这种智能指针类似于std::shared_ptr
,但不会增加引用计数。std::weak_ptr
主要用于防止std::shared_ptr
的循环引用问题,它可以观察std::shared_ptr
,但不拥有其引用。
这些智能指针类型都定义在<memory>
头文件中。
使用智能指针可以帮助你避免内存泄漏和其他与内存管理相关的错误。但是,虽然智能指针很有用,它们并不能解决所有的内存管理问题,例如,它们不能防止数组越界或解引用空指针等问题。因此,即使你在使用智能指针,仍然需要谨慎地处理指针和内存。
3.2 用法
以下是关于C++中各种智能指针的一些例子:
std::unique_ptr
:
std::unique_ptr
是一种独占所有权的智能指针,它不允许复制,但允许移动。移动std::unique_ptr
意味着所有权从一个std::unique_ptr
转移到另一个std::unique_ptr
。
你可以使用C++11中的std::move
函数来移动std::unique_ptr
。std::move
函数可以将其参数强制转换为右值,从而允许移动语义。
下面是一个例子:
#include <memory>
struct A {};
void foo() {
std::unique_ptr<A> ptr1(new A); // ptr1现在拥有一个A对象
std::unique_ptr<A> ptr2 = std::move(ptr1); // ptr2现在拥有这个A对象,ptr1不再拥有任何对象
// 现在我们可以使用ptr2,但不能使用ptr1
}
在这个例子中,我们首先创建了一个std::unique_ptr<A>
,它名为ptr1
,并管理一个A
对象。然后,我们使用std::move
函数将ptr1
的所有权移动给ptr2
。在这个移动操作后,ptr1
不再拥有任何对象,而ptr2
现在拥有这个A
对象。在这个移动操作后,我们可以继续使用ptr2
,但不能再使用ptr1
,因为ptr1
不再拥有任何对象。
std::shared_ptr
:
#include <memory>
void foo()
{
int a=5;
std::shared_ptr<int> ptr1(a);
delete ptr;
cout<<a;
}
//这个程序合法吗?
不幸的是,这个程序是不合法的,存在几个问题。
首先,std::shared_ptr<int> ptr1(a);
这一行是错误的。std::shared_ptr
的构造函数期望一个动态分配(例如通过new
关键字分配)的内存地址。在这种情况下,a
是一个栈变量,而不是一个动态分配的内存地址。因此,你应该使用new
关键字来分配一个新的int
,然后将其传递给std::shared_ptr
。比如,std::shared_ptr<int> ptr1(new int(a));
。
其次,你使用了delete ptr;
,这是错误的。delete
关键字用于释放通过new
关键字分配的内存。但是在这里,ptr
是一个智能指针,当它离开作用域或被重新赋值时,它会自动释放其所管理的内存。所以,你不应该(也不需要)对一个智能指针使用delete
。
最后,cout<<a;
在std::
命名空间前缺少了前缀,应该是std::cout<<a;
。而且这一行代码需要#include <iostream>
来使用std::cout
。
一个修正后的版本可能如下所示:
#include <iostream>
#include <memory>
void foo()
{
int a=5;
std::shared_ptr<int> ptr1(new int(a));
// 不需要手动删除 ptr1,当 ptr1 离开作用域时,它会自动释放内存
std::cout<<a;
}
注意,在这个修正后的版本中,std::shared_ptr
的内部指针是通过new
关键字动态分配的。在函数结束时,ptr1
离开作用域,std::shared_ptr
的析构函数会自动删除所指向的内存。
你的代码中创建了一个 shared_ptr<int>
对象 ptr2
,它指向一个包含 150 个 int
元素的数组。
shared_ptr<int> ptr2(new int [150]);
ptr2[100]=50;
//是否可以这样访问?
通过 ptr2[100]
的方式访问数组中的第 101 个元素,并将其赋值为 50。然而,这种使用方式是不正确的,因为 shared_ptr
并不支持通过索引方式访问其所管理的数组元素。
如果你想访问数组中的特定元素,你可以使用 get()
函数获取指向底层数组的原始指针,然后通过指针进行访问。以下是正确的方式:
shared_ptr<int> ptr2(new int[150]);
int* rawPtr = ptr2.get();
rawPtr[100] = 50;
在上述代码中,我们首先使用 get()
函数获取了 shared_ptr
的原始指针,并将其赋值给 rawPtr
。然后,我们可以使用 rawPtr
对底层数组进行访问,将值 50 赋给数组中的第 101 个元素。
需要注意的是,shared_ptr
对象仍然负责管理整个数组的内存,因此在 shared_ptr
的生命周期结束之前,不要手动释放底层数组的内存或者尝试使用 delete[]
删除它。当 shared_ptr
超出作用域或者手动释放时,它会自动释放底层数组的内存。
std::weak_ptr
:
std::shared_ptr
的循环引用问题通常发生在两个对象相互引用对方,形成一个引用环。如果使用std::shared_ptr
来管理这些对象,那么这些对象的引用计数永远不会变为0,因此它们永远不会被删除,从而导致内存泄漏。下面是一个循环引用的例子:
struct B; // 前向声明B
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::shared_ptr<A> a_ptr;
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
} // 这里 a 和 b 超出了作用域,但由于循环引用,内存不会被释放
在这个例子中,A和B通过std::shared_ptr
相互引用,形成了一个引用环,从而导致了内存泄漏。
要解决这个问题,一种方法是使用std::weak_ptr
。std::weak_ptr
是一种不会增加引用计数的智能指针,因此可以安全地用于循环引用的情况。在上面的例子中,我们可以将B中的std::shared_ptr<A>
改为std::weak_ptr<A>
,从而解决循环引用的问题:
struct B;
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 而不是 shared_ptr
};
int main() {
auto a = std::make_shared<A>();// 使用std::make_shared创建一个std::shared_ptr<A>
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
} // 这里 a 和 b 超出了作用域,由于没有循环引用,内存会被正确释放
在这个修改后的版本中,当a和b离开作用域时,引用计数变为0,所以它们会被正确删除,内存也会被正确释放。
4 auto
auto
是 C++11 引入的关键字,用于自动类型推断。它可以根据变量的初始化表达式推断出变量的类型,并在编译时确定。
auto
的用法如下:
-
自动类型推断:
auto variable = value; // 根据 value 的类型推断 variable 的类型
-
迭代器类型推断:
std::vector<int> vec = {1, 2, 3}; for (auto it = vec.begin(); it != vec.end(); ++it) { // 使用 auto 推断迭代器类型 }
-
函数返回类型推断:
auto func() { // 函数体 }
-
用于迭代器和指针解引用:
std::vector<int> vec = {1, 2, 3}; for (auto& element : vec) { // 使用 auto& 推断 element 的类型 } int* ptr = new int(10); auto value = *ptr; // 根据指针解引用的结果推断 value 的类型
-
使用匿名函数(lambda)表达式:
auto lambda = [](int x, int y) { return x + y; };
-
使用
auto
和结构化绑定(structured binding)将pair
分解为对应的元素。auto [a, b] = std::make_pair("a", 1);
5 一些stl容器
5.1 deque&list (Sequence)
std::deque
(双端队列)和 std::list
(双向链表)是 C++ 标准库提供的两种不同的容器类型,它们在不同的场景下具有不同的特点和用法。
std::deque(双端队列)
std::deque
是一种支持高效的随机访问和在两端插入/删除操作的容器。它是一个双向队列,可以在头部和尾部进行常量时间的插入和删除操作,同时也支持随机访问。
常用的 std::deque
操作包括:
-
插入和删除操作:
push_back(value)
: 在尾部插入一个元素。push_front(value)
: 在头部插入一个元素。pop_back()
: 删除尾部的元素。pop_front()
: 删除头部的元素。
-
随机访问:
operator[](index)
: 通过索引访问元素。at(index)
: 通过索引访问元素,会进行范围检查。
-
容量相关操作:
size()
: 返回容器中的元素数量。empty()
: 检查容器是否为空。clear()
: 清空容器中的所有元素。
std::list(双向链表)
std::list
是一种双向链表容器,它支持高效的在任意位置插入和删除元素,但不支持随机访问。在插入和删除元素频繁、需要维护大量迭代器的场景下,std::list
是一个很好的选择。
常用的 std::list
操作包括:
-
插入和删除操作:
push_back(value)
: 在尾部插入一个元素。push_front(value)
: 在头部插入一个元素。insert(pos, value)
: 在指定位置插入一个元素。erase(pos)
: 删除指定位置的元素。pop_back()
: 删除尾部的元素。pop_front()
: 删除头部的元素。
-
遍历和访问:
begin()
,end()
: 返回指向链表起始和结束的迭代器。front()
: 返回头部的元素。back()
: 返回尾部的元素。
-
容量相关操作:
size()
: 返回链表中的元素数量。empty()
: 检查链表是否为空。clear()
: 清空链表中的所有元素。
需要注意的是,由于 std::list
是基于链表实现的,因此它的随机访问效率较低,不支持像 std::deque
和 std::vector
那样的 operator[]
运算符和常量时间的随机访问。因此,在需要
5.2 map&set&unordered_map (Associative)
std::map
、std::set
和 std::unordered_map
是 C++ 标准库提供的关联容器,它们各自有不同的用途和特点。
std::map
std::map
是一个有序的关联容器,它存储键-值对,并根据键的排序对其进行自动排序。每个键在 std::map
中是唯一的。
常用的 std::map
操作包括:
-
插入和删除操作:
insert({key, value})
: 插入一个键值对。erase(key)
: 删除指定键的键值对。clear()
: 清空std::map
中的所有键值对。
-
查找和访问:
find(key)
: 查找指定键的元素,并返回指向该元素的迭代器。如果找不到,则返回指向std::map
结尾的迭代器。operator[](key)
: 通过键访问元素,如果键不存在则会插入一个默认值并返回对应的值。
-
遍历和访问:
begin()
,end()
: 返回指向std::map
起始和结束的迭代器。rbegin()
,rend()
: 返回反向迭代器,可以进行反向遍历。
std::set
std::set
是一个有序的关联容器,它存储唯一的键值,并根据键的排序对其进行自动排序。
常用的 std::set
操作包括:
-
插入和删除操作:
insert(value)
: 插入一个值。erase(value)
: 删除指定值。clear()
: 清空std::set
中的所有值。
-
查找和访问:
find(value)
: 查找指定值,并返回指向该值的迭代器。如果找不到,则返回指向std::set
结尾的迭代器。
-
遍历和访问:
begin()
,end()
: 返回指向std::set
起始和结束的迭代器。rbegin()
,rend()
: 返回反向迭代器,可以进行反向遍历。
std::unordered_map
std::unordered_map
是一个无序的关联容器,它存储键-值对,并根据键的哈希值对其进行快速访问。每个键在 std::unordered_map
中是唯一的。
常用的 std::unordered_map
操作包括:
- 插入和删除操作:
insert({key, value})
: 插入一个键值对。erase(key)
: 删除指定键的键值对。clear()
: 清空std::unordered_map
中的所有键值对。
- 查找和访问:
find(key)
: 查找指定键的元素,并返回指向该元素的迭代器。
5.3 vector的初始化方式
在 C++ 中,std::vector
是一个动态数组容器,提供了多种初始化方式。以下是一些常见的 std::vector
初始化方式:
-
默认初始化:
std::vector<int> vec; // 创建一个空的 vector,不包含任何元素
-
使用初始化列表初始化(C++11 及以上):
std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用大括号初始化列表初始化 vector
-
使用元素数量和默认值初始化:
std::vector<int> vec(5, 0); // 创建一个包含 5 个值为 0 的元素的 vector
-
使用迭代器范围初始化:
std::vector<int> source = {1, 2, 3, 4, 5}; std::vector<int> vec(source.begin(), source.end()); // 使用迭代器范围初始化 vector
-
使用拷贝构造函数初始化:
std::vector<int> source = {1, 2, 3, 4, 5}; std::vector<int> vec(source); // 使用拷贝构造函数初始化 vector
-
使用移动构造函数初始化(C++11 及以上):
std::vector<int> source = {1, 2, 3, 4, 5}; std::vector<int> vec(std::move(source)); // 使用移动构造函数初始化 vector
这些是 std::vector
常见的初始化方式。根据具体的需求,选择适当的初始化方式可以方便地创建和初始化 std::vector
对象。需要根据所使用的 C++ 版本以及特定的需求来选择最合适的初始化方式。
5.4 迭代器
![image-20230612221157542](https://s1.imagehub.cc/images/2023/06/13/image-20230612221157542.png)
![image-20230612221211545](https://s1.imagehub.cc/images/2023/06/13/image-20230612221211545.png)
6 关键字
6.1 friend 关键字
friend
是 C++ 中的一个关键字,用于在类之间进行友元关系的声明。通过 friend
,一个类可以授权其他类或函数访问其私有成员。
使用 friend
关键字可以实现以下几种情况:
-
类之间的友元关系:
- 一个类可以将另一个类声明为友元类,从而允许友元类访问其私有成员。
- 友元类的成员函数可以访问声明为友元的类的私有成员。
-
非成员函数的友元关系:
- 一个类可以将非成员函数声明为友元函数,从而允许友元函数访问其私有成员。
- 友元函数可以访问声明为友元的类的私有成员。
下面是一个示例,展示了 friend
的使用:
#include <iostream>
class MyClass {
private:
int privateData;
public:
MyClass() : privateData(42) {}
friend class FriendClass; // 将 FriendClass 声明为友元类
friend void FriendFunction(const MyClass& obj); // 将 FriendFunction 声明为友元函数
};
class FriendClass {
public:
void accessPrivateData(const MyClass& obj) {
std::cout << "FriendClass accessing private data: " << obj.privateData << std::endl;
}
};
void FriendFunction(const MyClass& obj) {
std::cout << "FriendFunction accessing private data: " << obj.privateData << std::endl;
}
int main() {
MyClass obj;
FriendClass friendObj;
friendObj.accessPrivateData(obj); // FriendClass 访问私有成员
FriendFunction(obj); // FriendFunction 访问私有成员
return 0;
}
在这个例子中,MyClass
类将 FriendClass
声明为友元类,并将 FriendFunction
声明为友元函数。这样,FriendClass
和 FriendFunction
就可以访问 MyClass
的私有成员 privateData
。
在 main
函数中,我们创建了一个 MyClass
对象 obj
和一个 FriendClass
对象 friendObj
。然后,我们通过 friendObj
调用 accessPrivateData
函数,以及直接调用 FriendFunction
,这两个函数都可以访问 MyClass
的私有成员 privateData
。
友元关系允许特定的类或函数访问另一个类的私有成员,但它们仅限于在特定的上下文中使用,应该谨慎使用,以避免破坏封装性和增加代码的复杂性。
friend
关键字在重载输入和输出流运算符时常用,它可以让非成员函数(通常是全局函数或其他类的成员函数)访问类的私有成员,从而实现自定义的输入和输出格式。
下面是一个示例,演示了如何在类中使用 friend
关键字来重载流插入运算符 <<
和流提取运算符 >>
:
#include <iostream>
class MyClass {
private:
int data;
public:
MyClass(int value) : data(value) {}
// 声明友元函数,用于重载流插入运算符
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj);
// 声明友元函数,用于重载流提取运算符
friend std::istream& operator>>(std::istream& is, MyClass& obj);
};
// 定义友元函数,重载流插入运算符
std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
os << "MyClass data: " << obj.data;
return os;
}
// 定义友元函数,重载流提取运算符
std::istream& operator>>(std::istream& is, MyClass& obj) {
is >> obj.data;
return is;
}
int main() {
MyClass obj(42);
// 使用流插入运算符输出对象
std::cout << obj << std::endl; // 输出: MyClass data: 42
// 使用流提取运算符从输入流中提取数据到对象
std::cout << "Enter a value: ";
std::cin >> obj;
// 使用流插入运算符再次输出对象
std::cout << obj << std::endl;
return 0;
}
在这个例子中,我们定义了一个 MyClass
类,它包含一个私有成员变量 data
。然后,我们在类内部声明了两个友元函数 operator<<
和 operator>>
,并在类外部定义它们。
operator<<
重载了流插入运算符,允许使用 std::cout
输出 MyClass
对象的数据。operator>>
重载了流提取运算符,允许从输入流中提取数据并存储到 MyClass
对象的 data
成员中。
在 main
函数中,我们创建了一个 MyClass
对象 obj
,使用流插入运算符 <<
输出对象的数据,并使用流提取运算符 >>
从用户输入中读取数据并存储到对象中,最后再次使用流插入运算符 <<
输出更新后的对象数据。
通过使用 friend
关键字,我们可以让非成员函数(友元函数)访问类的私有成员,从而实现自定义的输入和输出格式。
6.2 constexpr 关键字
constexpr
是 C++ 中的关键字,用于声明可以在编译时求值的常量表达式(常量表达式是指在编译时能够计算出结果的表达式)。
使用 constexpr
可以将函数或变量声明为常量表达式,从而在编译时进行计算和优化。这有助于提高程序的性能和效率,并允许在编译时进行一些复杂的计算。
下面是一些 constexpr
的常见用法:
-
constexpr
变量:constexpr int max = 100;
在这个例子中,
max
被声明为constexpr
变量,表示它是一个编译时常量,可以在编译时进行求值和优化。 -
constexpr
函数:constexpr int square(int x) { return x * x; }
在这个例子中,
square
函数被声明为constexpr
函数,表示它可以在编译时进行求值,并且返回的结果也是一个编译时常量。 -
constexpr
枚举:enum class Color : int { Red = 0, Green = 1, Blue = 2 }; constexpr Color primaryColor = Color::Red;
在这个例子中,我们定义了一个枚举类型
Color
,并使用constexpr
将其中的一个枚举值Red
声明为常量。
需要注意的是,constexpr
对象或函数必须满足一些限制条件,以便在编译时进行求值。例如,constexpr
函数必须具有确定的返回值,并且在其所有可能的参数值上都能产生相同的结果。
6.3 mutable 关键字
mutable
是一个关键字,在 C++ 中用于修饰类的成员变量。通过使用 mutable
关键字,可以在常量成员函数中修改被标记为 mutable
的成员变量。
在普通的成员函数中,如果函数被声明为 const
,则该函数不能修改类的成员变量,以保证对象的逻辑常量性(logical constness)。然而,有时候我们可能希望在某些情况下仍然能够修改成员变量,即使在常量成员函数中。这时就可以使用 mutable
关键字。
下面是一个简单的示例,演示了 mutable
的用法:
class Counter {
public:
Counter() : count(0) {}
int getCount() const {
// 在常量成员函数中修改被标记为 mutable 的成员变量
count++;
return count;
}
private:
mutable int count;
};
int main() {
Counter c;
int currentCount = c.getCount(); // 调用常量成员函数,并修改 mutable 成员变量
std::cout << "Current count: " << currentCount << std::endl;
return 0;
}
在这个示例中,我们定义了一个名为 Counter
的类,其中包含一个私有成员变量 count
,被标记为 mutable
。
在 Counter
类的常量成员函数 getCount
中,我们可以修改被标记为 mutable
的 count
成员变量。这允许我们在常量成员函数中修改该变量,即使该函数被声明为 const
。
在 main
函数中,我们创建了一个 Counter
对象 c
,并调用 getCount
方法。由于 getCount
是一个常量成员函数,本质上不会修改对象的状态,但由于 count
被标记为 mutable
,我们仍然可以在函数内部递增 count
的值。
输出结果显示了当前的计数值,表明成功地在常量成员函数中修改了 count
。
需要注意的是,虽然 mutable
允许在常量成员函数中修改成员变量,但仍然要谨慎使用。应该确保修改 mutable
成员变量不会影响类的逻辑常量性,并且在设计时考虑清楚是否真正需要在常量成员函数中修改成员变量。
6.4 final 关键字
final
是一个关键字,在 C++11 及以后的标准中引入,用于修饰类、成员函数和虚函数。
-
final
修饰类:final
修饰类时,表示该类是最终类,不允许其他类继承它。
class Base final { /* ... */ }; // 该类 Base 是最终类,不允许其他类继承它
-
final
修饰成员函数:final
修饰成员函数时,表示该函数是最终版本,不允许子类重写该函数。
class Base { virtual void foo() final { /* ... */ } }; // 子类无法重写 Base 类中的 foo() 函数
-
final
修饰虚函数:final
修饰虚函数时,表示该虚函数是最终版本,不允许子类再次重写它。
class Base { virtual void foo() { /* ... */ } }; class Derived : public Base { void foo() final { /* ... */ } }; // Derived 类中的 foo() 函数是最终版本,子类无法再次重写它
7 stream
7.1 文件输入输出流
在 C++ 中,文件输入输出流是用于读取和写入文件数据的流类。文件流类主要由 std::ifstream
(用于读取文件)和 std::ofstream
(用于写入文件)组成,它们都是派生自基类 std::iostream
。
下面是一个简单的示例,演示了如何使用文件输入输出流读取和写入文件:
#include <iostream>
#include <fstream>
int main() {
std::string filename = "data.txt";
// 写入文件
std::ofstream outFile(filename); // 创建输出文件流对象
if (outFile.is_open()) { // 检查文件是否成功打开
outFile << "Hello, File!" << std::endl;
outFile << "This is a line of text." << std::endl;
outFile.close(); // 关闭文件流
std::cout << "File write completed." << std::endl;
} else {
std::cout << "Unable to open file for writing." << std::endl;
}
// 读取文件
std::ifstream inFile(filename); // 创建输入文件流对象
if (inFile.is_open()) { // 检查文件是否成功打开
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close(); // 关闭文件流
std::cout << "File read completed." << std::endl;
} else {
std::cout << "Unable to open file for reading." << std::endl;
}
return 0;
}
在这个示例中,我们首先创建了一个输出文件流对象 std::ofstream
,并通过构造函数参数指定要写入的文件名。然后,我们使用流插入运算符 <<
将数据写入文件。最后,我们关闭文件流。
接下来,我们创建一个输入文件流对象 std::ifstream
,并通过构造函数参数指定要读取的文件名。然后,我们使用 std::getline
函数从文件中逐行读取数据,并将每行数据打印到标准输出。最后,我们关闭文件流。
需要注意的是,文件流的操作可能会引发异常,因此建议使用适当的异常处理机制,如 try-catch
块来处理可能的异常情况。
在使用文件流时,确保文件路径和权限正确,并始终检查文件的打开状态,以确保成功打开文件并进行读写操作。
7.2 cerr&clog
cerr
和 clog
是两个标准输出流对象,用于向标准错误输出(Standard Error Output)打印信息。
cerr
是 std::ostream
类型的全局对象,它关联到标准错误输出流,通常用于输出错误和异常相关的信息。使用 cerr
输出的内容会直接发送到标准错误输出,而不会经过缓冲区。
clog
也是 std::ostream
类型的全局对象,它关联到标准错误输出流,类似于 cerr
。不同之处在于,使用 clog
输出的内容会先经过缓冲区,然后在适当的时机刷新到标准错误输出。
下面是一个简单的示例,演示了 cerr
和 clog
的用法:
#include <iostream>
int main() {
int x = 42;
// 使用 cerr 输出错误信息
std::cerr << "An error occurred!" << std::endl;
std::cerr << "The value of x is: " << x << std::endl;
// 使用 clog 输出日志信息
std::clog << "Logging information..." << std::endl;
std::clog << "The value of x is: " << x << std::endl;
return 0;
}
在这个示例中,我们使用 cerr
输出了一个错误信息,并打印了变量 x
的值。这些信息会直接发送到标准错误输出流,可以在控制台或日志文件中查看。
然后,我们使用 clog
输出了一些日志信息,并打印了变量 x
的值。这些信息会先经过缓冲区,然后在适当的时机刷新到标准错误输出流。这可以帮助在某些情况下更好地控制输出的顺序和性能。
需要注意的是,cerr
和 clog
都是线程安全的,可以在多线程环境中使用。它们都是 std::ostream
的对象,因此可以使用 <<
运算符和其他流操作符进行输出。
8 继承&多态
8.1 public&protected&private
public
、protected
和 private
是C++中的访问控制修饰符,用于控制类的成员的访问权限。
这些访问控制修饰符的作用是限制类中成员的可见性和访问级别,以实现封装性和数据隐藏的原则。
以下是对每个访问控制修饰符的简要说明:
public
:public
成员在类内外均可访问。它们的成员函数和数据成员可以从任何地方进行访问。公共成员是类对外的接口,用于与外部代码进行交互。
class MyClass {
public:
int publicMember; // 公共数据成员
void publicMethod() {
// 公共成员函数
}
};
protected
:protected
成员在类内部可访问,在派生类中也可访问,但在类外部不可访问。受保护的成员通常用于基类对派生类提供的接口,以及在继承关系中进行数据封装。
class MyBaseClass {
protected:
int protectedMember; // 受保护的数据成员
void protectedMethod() {
// 受保护的成员函数
}
};
class MyDerivedClass : public MyBaseClass {
public:
void accessProtectedMember() {
protectedMember = 10; // 在派生类中访问受保护的成员
}
};
private
:private
成员只能在类内部访问,对于类的外部和派生类来说是不可访问的。私有成员用于封装类的内部实现细节,提供数据隐藏和内部操作的安全性。
class MyClass {
private:
int privateMember; // 私有数据成员
void privateMethod() {
// 私有成员函数
}
};
需要注意的是,上述访问控制修饰符可以用于类的数据成员和成员函数,它们的访问级别是适用于整个成员。默认情况下,类中的成员(包括类的定义)的访问级别是 private
。
访问控制修饰符的选择取决于设计意图和数据封装的要求。通过合理的访问控制,可以确保数据的安全性和代码的可维护性。
8.2 构造函数
是类的构造函数不会被继承。
以下是一个简单的示例来说明类构造函数不被继承:
#include <iostream>
class Base {
public:
Base(int value) {
std::cout << "Base constructor called with value: " << value << std::endl;
}
};
class Derived : public Base {
public:
Derived(int value) : Base(value) {
std::cout << "Derived constructor called with value: " << value << std::endl;
}
};
int main() {
Derived obj(42);
return 0;
}
在上面的示例中,Derived
类继承自 Base
类。虽然 Base
类有一个带参数的构造函数,但它不会被继承到 Derived
类。然而,在 Derived
类的构造函数中,我们可以通过调用基类构造函数 Base(value)
来初始化从 Base
类继承的成员变量。
总结起来,类的构造函数不会被继承到派生类。派生类需要自己定义构造函数来初始化自己的成员变量,并可以通过调用基类构造函数来初始化从基类继承的成员变量。
explicit 用法
"explicit" 是C++中的关键字,用于修饰单参数构造函数,以防止隐式类型转换。它告诉编译器只允许显式调用该构造函数,而不允许隐式转换。
当一个构造函数被声明为 explicit 时,它将不再被用于隐式转换。相反,只能通过显式的方式(即使用构造函数的名称)来创建对象或进行类型转换。
下面是一个示例,演示了 explicit 关键字的使用:
class MyClass {
public:
int value;
// 显式构造函数
explicit MyClass(int val) : value(val) {
}
};
void func(MyClass obj) {
// ...
}
int main() {
MyClass obj1(42); // 直接调用构造函数,隐式转换是不允许的
MyClass obj2 = 10; // 错误,禁止隐式类型转换
MyClass obj3 = MyClass(15); // 显式调用构造函数进行类型转换
func(obj1); // 正确,隐式转换是允许的
func(20); // 错误,禁止隐式类型转换
return 0;
}
在上面的示例中,MyClass
类的构造函数被声明为 explicit,这意味着只能通过显式调用构造函数来创建对象或进行类型转换。直接使用单个参数调用构造函数进行隐式转换是不允许的。例如,MyClass obj2 = 10;
这样的隐式转换是错误的,需要使用显式方式 MyClass obj3 = MyClass(15);
来进行类型转换。
需要注意的是,当一个构造函数被声明为 explicit 时,它还可以防止某些不期望的隐式类型转换,提高代码的清晰性和类型安全性。
总结起来,explicit 关键字用于修饰单参数构造函数,禁止隐式类型转换,只允许通过显式的方式来调用构造函数或进行类型转换。
8.3 多态
多态性(Polymorphism)是面向对象编程的一个重要概念,它允许使用基类指针或引用来调用派生类的特定实现。多态性通过动态绑定函数调用,根据对象的实际类型来选择适当的函数实现。
在多态性的实现中,关键的要素是虚函数(Virtual Function)和基类指针或引用。
-
虚函数(Virtual Function):在基类中声明虚函数时,使用
virtual
关键字。虚函数允许在派生类中重写(覆盖)基类的函数实现。通过虚函数,可以根据对象的实际类型来动态绑定函数调用。 -
基类指针或引用(Base Class Pointer or Reference):可以使用基类指针或引用来引用派生类的对象。当基类指针或引用调用虚函数时,会根据实际对象的类型来选择适当的函数实现。
下面是一个示例,展示多态性的使用:
class Shape {
public:
virtual void draw() {
cout << "Drawing a shape" << endl;
}
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
cout << "Drawing a rectangle" << endl;
}
};
在上述示例中,Shape
类是基类,Circle
和 Rectangle
类是派生类。基类 Shape
中声明了虚函数 draw()
,而派生类 Circle
和 Rectangle
对该函数进行了重写。
通过使用基类指针或引用,可以实现多态性:
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
shape1->draw(); // 输出:Drawing a circle
shape2->draw(); // 输出:Drawing a rectangle
在上述代码中,shape1
和 shape2
是基类 Shape
的指针,分别指向 Circle
和 Rectangle
对象。当调用 draw()
函数时,会根据实际对象的类型来选择正确的函数实现。
多态性通过动态绑定函数调用,提供了灵活性和可扩展性。它允许以统一的方式处理不同类型的对象,使得代码更加通用和可维护。
8.4 Upcasting and Downcasting
8.4.1 Upcasting and Downcasting 例子
-
Upcasting 例子:
假设我们有两个类:一个基类Base,和一个从Base继承的子类Derived。这是Upcasting的例子:
class Base { public: virtual void print() { cout << "Base" << endl; } }; class Derived : public Base { public: void print() override { cout << "Derived" << endl; } }; int main() { Derived d; Base& b = d; // Upcasting b.print(); // Outputs: Derived }
在这个例子中,我们首先创建了一个Derived类的对象
d
,然后我们将其Upcast到Base类的引用b
。然后,我们调用print
方法。由于print
是一个虚函数,所以实际上调用的是Derived类的print
方法,这就是多态的一个示例。 -
Downcasting 例子:
Downcasting在C++中相当复杂,因为需要使用动态转型(dynamic_cast)。这是一个Downcasting的例子:
class Base { public: virtual void print() { cout << "Base" << endl; } }; class Derived : public Base { public: void print() override { cout << "Derived" << endl; } void specialFunction() { cout << "Special Function" << endl; } }; int main() { Base* b = new Derived(); // Upcasting b->print(); // Outputs: Derived Derived* d = dynamic_cast<Derived*>(b); // Downcasting if(d != nullptr) { d->specialFunction(); // Outputs: Special Function } }
在这个例子中,我们首先创建了一个Derived类的对象并Upcast它为Base类的指针
b
。然后我们将b
使用dynamic_cast
Downcast为Derived类的指针d
。这是安全的,因为b
实际上指向的是一个Derived对象。然后我们调用specialFunction
,这是Base类中没有但是Derived类中有的方法。
请注意,dynamic_cast
会检查Downcasting是否安全。如果不安全(例如,如果b
实际上并没有指向一个Derived对象),dynamic_cast
将返回nullptr
。在这种情况下,尝试调用specialFunction
可能会导致未定义的行为。因此,我们在调用specialFunction
之前检查d
是否为nullptr
。
8.4.2 static_cast
在C++中,static_cast
是一种编译时类型转换,可以用于基本数据类型之间的转换,或者由类层次结构内的指针或引用进行向上或向下的转换。
-
向上转型(Upcasting):
static_cast
可以很容易地进行向上转型,这是自然的,安全的,因为子类对象可以被看作是基类对象:class Base { public: virtual void print() { cout << "Base" << endl; } }; class Derived : public Base { public: void print() override { cout << "Derived" << endl; } }; int main() { Derived d; Base* b = static_cast<Base*>(&d); // Upcasting b->print(); // Outputs: Derived }
在上述代码中,我们创建了一个Derived对象
d
,然后我们将其通过static_cast
向上转型为Base类的指针b
。因为print
是一个虚函数,我们调用的实际上是Derived类的print
方法。 -
向下转型(Downcasting):
static_cast
也可以用于向下转型,但这是不安全的,因为如果实际对象的类型不是目标类型,或者并非目标类型的子类型,这种类型转换可能会导致未定义的行为:class Base { public: virtual void print() { cout << "Base" << endl; } }; class Derived : public Base { public: void print() override { cout << "Derived" << endl; } void specialFunction() { cout << "Special Function" << endl; } }; int main() { Base* b = new Derived(); // Upcasting b->print(); // Outputs: Derived Derived* d = static_cast<Derived*>(b); // Downcasting d->specialFunction(); // Outputs: Special Function }
在这个例子中,我们首先创建了一个Derived类的对象并将其Upcast为Base类的指针
b
。然后我们使用static_cast
将b
Downcast为Derived类的指针d
。然后我们调用specialFunction
,这是在Base类中没有但是在Derived类中有的方法。
请注意,static_cast
不进行运行时类型检查,如果b
实际上并没有指向一个Derived对象,那么结果将是未定义的。这就是为什么在进行Downcasting时更推荐使用dynamic_cast
,因为它会进行运行时类型检查,确保类型转换的安全性。
8.4.3 sidecasting 同级转换
在C++中,如果你有两个继承自同一个基类的类,你不能直接用dynamic_cast
将一个子类的对象转换为另一个子类。你必须首先将对象向上转型(upcast)为基类,然后再向下转型(downcast)为目标子类。
下面是一个例子:
class Base {
public:
virtual void print() { cout << "Base" << endl; }
};
class Derived1 : public Base {
public:
void print() override { cout << "Derived1" << endl; }
};
class Derived2 : public Base {
public:
void print() override { cout << "Derived2" << endl; }
};
int main() {
Derived1 d1;
Base* b = &d1; // Upcasting
Derived2* d2 = dynamic_cast<Derived2*>(b); // Downcasting
if (d2 != nullptr) {
d2->print(); // Outputs: Derived2
} else {
cout << "The downcasting was not successful." << endl;
}
}
在这个例子中,Derived1
的对象首先被向上转型为Base
的指针b
,然后尝试使用dynamic_cast
将b
向下转型为Derived2
的指针d2
。然而,这个向下转型会失败,因为b
实际上并不指向Derived2
的对象,因此dynamic_cast
将返回nullptr
。在尝试调用Derived2
的方法之前,我们检查d2
是否为nullptr
,因此程序不会崩溃。
总的来说,在C++中,即使两个类有相同的基类,你也不能直接用dynamic_cast
将一个子类的对象转换为另一个子类。你需要首先向上转型为基类,然后再向下转型为目标子类。
8.5 子类的析构
当删除一个对象时,首先调用该派生类的析构函数,然后调用上一层基类的析构函数,依次类推,直到到达最顶层的基类的析构函数为止。
以下是一个示例,展示了析构函数在继承中的情况:
#include <iostream>
// 基类
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
}
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
// 派生类
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor" << std::endl;
}
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Derived obj; // 创建派生类对象
return 0;
}
在上面的示例中,我们定义了一个基类 Base
和一个派生类 Derived
,派生类 Derived
公开继承了基类 Base
。在 main()
函数中,我们创建了一个派生类对象 obj
。
当程序运行时,创建派生类对象时的输出如下所示:
Base constructor
Derived constructor
在程序结束时,销毁派生类对象时的输出如下所示:
Derived destructor
Base destructor
从输出可以看出,析构函数的调用顺序与构造函数的调用顺序相反。首先调用派生类的析构函数,然后自动调用基类的析构函数。
需要注意的是,基类的析构函数应当声明为虚函数(virtual),以便在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,确保正确的对象清理。
9 template&typename
9.1 template
模板是 C++ 中强大的特性,它允许编写通用的代码,可以在不同的类型上进行重用。
使用模板,可以创建泛型代码,使得函数和类能够处理多种类型的数据,而不需要针对每种类型编写单独的代码。
以下是一些常见的模板用法:
-
函数模板(Function Templates):
template <typename T> T add(T a, T b) { return a + b; }
在这个例子中,
add
是一个函数模板,使用了模板参数T
,表示函数可以接受任意类型的参数。通过在函数体中使用模板参数T
,我们可以实现通用的加法操作。 -
类模板(Class Templates):
template <typename T> class Stack { private: std::vector<T> data; public: void push(T item) { data.push_back(item); } T pop() { T item = data.back(); data.pop_back(); return item; } };
在这个例子中,
Stack
是一个类模板,使用了模板参数T
,表示类可以处理任意类型的数据。通过在类中使用模板参数T
,我们可以创建一个通用的堆栈数据结构,可以在运行时决定堆栈存储的具体类型。 -
模板特化(Template Specialization):
template <typename T> void printType() { std::cout << "Generic type" << std::endl; } template <> void printType<int>() { std::cout << "Specialized type: int" << std::endl; }
在这个例子中,
printType
是一个函数模板,用于打印类型信息。我们可以为特定的类型(如int
)提供特化版本,以实现特定的行为。在这种情况下,当调用printType
函数并传递int
类型的参数时,将使用特化版本进行处理。 -
模板参数推断(Template Argument Deduction):
template <typename T> void print(T item) { std::cout << item << std::endl; }
在这个例子中,
print
是一个函数模板,接受一个参数并将其打印到标准输出。当调用print
函数时,编译器会根据传递的实际参数类型来推断模板参数T
的类型,而不需要显式指定。、
9.2 typename的用法
在 C++ 中,typename
关键字通常用于模板编程中,用于指示依赖类型(dependent type)是一个类型。
当在模板中使用嵌套类型或依赖于模板参数的类型时,编译器无法确定该标识符是一个类型还是一个静态成员变量或函数。为了告诉编译器这是一个类型,我们使用 typename
关键字来明确指示。
下面是一些使用 typename
的常见情况:
-
声明模板类型别名:
template <typename T> using MyVector = std::vector<T>;
-
声明嵌套类型:
template <typename T> void printSize(const T& container) { typename T::size_type size = container.size(); std::cout << "Size: " << size << std::endl; }
-
在模板成员函数中访问模板参数的类型:
template <typename T> class MyClass { public: void foo() { typename T::value_type value; // ... } };
T::value_type
可以是一个表示容器元素类型的类型。在标准库中,许多容器类(如 std::vector
、std::list
、std::set
等)都定义了一个名为 value_type
的嵌套类型,用于表示容器中存储的元素的类型。
下面是一个示例,演示了 T::value_type
的使用:
#include <iostream>
#include <vector>
template <typename T>
void printFirstElement(const T& container) {
typename T::value_type firstElement = container.front();
std::cout << "First element: " << firstElement << std::endl;
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
printFirstElement(numbers); // 输出: First element: 1
return 0;
}
在这个示例中,我们定义了一个函数 printFirstElement
,它接受一个容器 container
作为参数,并使用 T::value_type
声明了一个名为 firstElement
的变量。通过调用 container.front()
,我们获取容器的第一个元素,并将其存储到 firstElement
中。然后,我们将该元素打印到标准输出。
在 main
函数中,我们创建了一个 std::vector<int>
类型的容器 numbers
,并将一些整数存储在其中。然后,我们调用 printFirstElement
函数,并将 numbers
作为参数传递给它。函数会打印容器的第一个元素。
需要注意的是,不同的容器类型可能具有不同的 value_type
。例如,std::vector<int>::value_type
是 int
,而 std::vector<double>::value_type
是 double
。因此,通过使用 T::value_type
,我们可以在模板中独立于具体容器类型地操作容器元素的类型。
10 函数指针
函数指针(Function Pointers)是指向函数的指针变量,它可以用于存储函数的地址并通过该指针调用函数。函数指针可以像普通变量一样传递、存储和使用,为动态调用函数提供了一种灵活的机制。
以下是函数指针的用法示例:
#include <iostream>
// 声明一个函数指针类型
typedef int (*ArithmeticFunction)(int, int);
// 加法函数
int add(int a, int b) {
return a + b;
}
// 减法函数
int subtract(int a, int b) {
return a - b;
}
int M(int x,int y,int (*p)(int,int)){ //参数也可以是函数指针
return p(x,y);
}
int main() {
// 声明函数指针变量,并初始化为指向 add 函数的地址
ArithmeticFunction funcPtr = add;
//等价于 auto funcPtr = add;
//推导为 int (*)(int, int)
// 使用函数指针调用函数
int result = funcPtr(5, 3);
std::cout << "Result: " << result << std::endl;
// 修改函数指针指向 subtract 函数的地址
funcPtr = subtract;
result = funcPtr(5, 3);
cout<<M(a,b,add)<<' '<<M(a,b,subtract);
return 0;
}
11 static
- 静态局部变量:在函数内部声明的静态局部变量具有静态生命周期,即在程序的整个执行过程中都存在,并且只会被初始化一次。静态局部变量对于函数的每次调用都是共享的。
void foo() {
static int count = 0; // 静态局部变量
count++;
std::cout << "Count: " << count << std::endl;
}
int main() {
foo(); // Count: 1
foo(); // Count: 2
foo(); // Count: 3
return 0;
}
在上面的示例中,函数 foo()
内部声明了一个静态局部变量 count
,并在每次调用 foo()
时递增计数并输出。由于静态局部变量具有静态生命周期,每次调用 foo()
时,count
的值会保留并继续递增。
- 静态函数:在函数的声明和定义中使用 "static" 关键字可以将函数声明为静态函数,这将限制函数的作用域只在当前文件内可见,不会对其他文件产生链接。
static void bar() {
std::cout << "This is a static function." << std::endl;
}
int main() {
bar(); // 可以直接调用静态函数
return 0;
}
在上面的示例中,函数 bar()
声明为静态函数,可以直接在当前文件内调用,而不需要通过函数的外部链接。
- 静态成员函数:在类中声明和定义的静态成员函数也使用 "static" 关键字。静态成员函数不属于特定对象,而是属于类本身。可以直接通过类名访问,而无需创建对象实例。
class MyClass {
public:
static void baz() {
std::cout << "This is a static member function." << std::endl;
}
};
int main() {
MyClass::baz(); // 可以直接通过类名调用静态成员函数
return 0;
}
在上面的示例中,类 MyClass
中定义了一个静态成员函数 baz()
,可以直接通过类名调用该函数,而无需创建类的对象实例。