c++ day3
今天的复习感觉会挺轻松。
#ifndef
是 C++ 中的一个预处理指令,用于条件编译。它通常与 #define
和 #endif
一起使用,用于包含或排除某个代码块,以防止重复包含头文件。
当使用 #include
指令将头文件包含到源文件中时,存在一种潜在的问题,即多个源文件可能都包含了同一个头文件。这种情况下,如果没有适当的预防措施,可能会导致重复定义的错误。
为了解决这个问题,C++ 提供了条件编译指令 #ifndef
(也称为条件预处理指令)。它允许我们根据一个标识符的定义状态来决定是否编译某段代码。常见的用法是在头文件中使用 #ifndef
配合 #define
和 #endif
。
下面是详细的解释和用法示例:
-
#ifndef
:这是条件编译指令的开始部分。它接受一个标识符作为参数,通常是一个宏定义的名称。在预处理阶段,如果该标识符未定义,条件就成立,代码块将会被编译。 -
#define
:这是条件编译指令的一部分。它用于定义标识符,以表示某个代码块正在被编译。在通常的用法中,我们使用与#ifndef
相同的标识符作为参数。这样,当头文件第一次被包含时,该标识符将被定义。 -
#endif
:这是条件编译指令的结束部分。它标志着条件编译代码块的结束。
通过将这三个指令结合使用,可以确保头文件只被编译一次,即使它在多个源文件中被包含。
下面是一个示例:
假设有一个名为 header.h
的头文件,其中包含一些函数的声明和定义:
1 // header.h 2 3 #ifndef HEADER_H 4 #define HEADER_H 5 6 void foo(); 7 void bar(); 8 9 #endif
在上述代码中,HEADER_H
是我们自定义的标识符。如果在包含 header.h
之前该标识符未定义,那么就会执行 #ifndef
和 #endif
之间的代码。
现在,假设有两个源文件 source1.cpp
和 source2.cpp
都需要使用 header.h
中的函数。
1 // source1.cpp 2 3 #include "header.h" 4 5 int main() { 6 foo(); 7 return 0; 8 }
1 // source2.cpp 2 3 #include "header.h" 4 5 void bar() { 6 // 函数实现 7 }
当编译 source1.cpp
和 source2.cpp
时,它们都会包含 header.h
。由于 header.h
中的 #ifndef
和 #define
保护,头文件只会被编译一次。这样,foo
和 bar
的定义只会出现一次,并且不会导致重复定义的错误。
总结一下:
#ifndef
用于检查一个标识符是否未定义。#define
用于定义标识符,表示代码块正在被编译。#endif
标志着条件编译代码块的结束。
通过合理使用 #ifndef
,可以避免头文件的重复包含和重复定义的问题,确保代码的正确编译和执行。
异常类
异常类在 C++ 中用于处理异常情况。它们是从 std::exception
类派生而来的,可以通过自定义异常类来表示特定的异常类型。下面是异常类的定义和使用方式:
1 #include <exception> 2 3 class MyException : public std::exception { 4 public: 5 // 构造函数,通常接受一个字符串作为异常信息 6 MyException(const char* message) : m_message(message) {} 7 8 // 重写基类的 what() 函数,返回异常信息 9 virtual const char* what() const noexcept { 10 return m_message; 11 } 12 13 private: 14 const char* m_message; 15 };
在上述代码中,我们定义了一个名为 MyException
的自定义异常类,它继承自 std::exception
。异常类通常具有一个构造函数,用于接收异常信息,并将其存储在成员变量中。我们还重写了 what()
函数,该函数是基类 std::exception
的成员函数,用于返回异常信息。
使用自定义异常类时,可以通过抛出异常的方式来表示异常情况,并在异常处理代码中捕获并处理它们。下面是一个示例:
1 #include <iostream> 2 3 void foo() { 4 throw MyException("Something went wrong!"); 5 } 6 7 int main() { 8 try { 9 foo(); 10 } catch (const MyException& e) { 11 std::cout << "Exception caught: " << e.what() << std::endl; 12 } 13 14 return 0; 15 }
在上述代码中,foo()
函数抛出了一个 MyException
异常对象,携带了异常信息。在 main()
函数中,我们使用 try-catch
块来捕获并处理异常。当异常被抛出时,程序会跳转到 catch
块中,并执行相应的异常处理代码。
在上面的示例中,我们捕获了 MyException
类型的异常,并通过调用 e.what()
来获取异常信息并输出。
自定义异常类使得我们能够更好地组织和处理不同类型的异常,以便在程序执行过程中捕获和处理特定的异常情况。
终于学习到递归函数了
首先来理解下什么是递归,大家都听说一个故事吧。
从前有座山,山里有座庙,庙里有个老和尚和小和尚,老和尚对小和尚说,从前有座山,山里有座庙,庙里有个老和尚和小和尚,老和尚对小和尚说,从前有座山......
这就是一种递归
在递归函数中,可以存在两种类型的递归:直接递归和间接递归。
- 直接递归(Direct Recursion):直接递归是指递归函数在自身的定义体内直接调用自身。换句话说,函数直接调用自身作为一部分。这种递归形式是最常见的。
下面是一个示例,展示了直接递归的情况:
1 void directRecursion() { 2 // 其他代码... 3 4 // 直接调用自身 5 directRecursion(); 6 7 // 其他代码... 8 }
在上述代码中,directRecursion()
函数在其定义体内直接调用了自身。
- 间接递归(Indirect Recursion):间接递归是指递归函数通过一系列函数调用,最终又回到自身。换句话说,多个函数之间形成了一个环状调用,最终导致递归。
下面是一个示例,展示了间接递归的情况:
1 void functionB(); // 函数的声明 2 3 void functionA() { 4 // 其他代码... 5 6 // 调用函数B 7 functionB(); 8 9 // 其他代码... 10 } 11 12 void functionB() { 13 // 其他代码... 14 15 // 调用函数A 16 functionA(); 17 18 // 其他代码... 19 }
在上述代码中,functionA()
和 functionB()
两个函数通过相互调用形成了一个间接递归。
无论是直接递归还是间接递归,递归函数都需要正确设置终止条件,以确保递归能够在某个条件下结束,避免无限递归。在递归过程中,终止条件的判断非常重要,以确保递归能够正确结束。
递归的选择取决于问题的性质和逻辑结构。有时候,直接递归更直观和自然,而在其他情况下,间接递归可能更适合解决特定的问题。
递归的数学函数
归在数学函数中有广泛的应用,以下是一些常见的递归数学函数的示例:
- 阶乘(Factorial):阶乘是一个经典的递归函数示例。n 的阶乘(表示为 n!)定义为从 1 到 n 的所有正整数的乘积。阶乘函数可以使用递归来计算。
1 int factorial(int n) { 2 if (n == 0) 3 return 1; 4 return n * factorial(n - 1); 5 }
- 斐波那契数列(Fibonacci Sequence):斐波那契数列是一个递归定义的数列,其中每个数字都是前两个数字的和。数列的前两个数字通常定义为 0 和 1。
1 int fibonacci(int n) { 2 if (n <= 1) 3 return n; 4 return fibonacci(n - 1) + fibonacci(n - 2); 5 }
- 幂函数(Power Function):幂函数用于计算一个数的指定次幂。可以使用递归来实现幂函数。
1 double power(double base, int exponent) { 2 if (exponent == 0) 3 return 1; 4 if (exponent > 0) 5 return base * power(base, exponent - 1); 6 else 7 return 1 / (base * power(base, -exponent - 1)); 8 }
- 最大公约数(Greatest Common Divisor):最大公约数是两个或多个整数的最大公约数。可以使用递归的欧几里得算法来计算最大公约数。
1 int gcd(int a, int b) { 2 if (b == 0) 3 return a; 4 return gcd(b, a % b); 5 }
这些是一些常见的数学函数,它们使用递归来解决具有递归性质的问题。在实际使用中,需要根据具体问题选择适当的递归方法,并确保正确设置递归的终止条件,以避免无限递归和其他潜在的问题。
递归的归纳证明是数学和计算机科学中常用的证明技巧之一。它用于证明递归定义的数学对象或递归算法的性质。
下面是递归的归纳证明的基本步骤:
-
基本情况(Base Case):首先证明递归的基本情况。即证明当满足最小的输入条件时,性质成立。通常这是一个简单且显而易见的情况。
-
归纳假设(Inductive Hypothesis):假设递归定义或算法对于某个规模较小的输入是正确的。这就是归纳假设的核心,它假设了性质在规模较小的情况下成立。
-
归纳步骤(Inductive Step):证明在给定归纳假设下,当输入规模增加时,性质仍然成立。这一步骤通常包括两个关键要素:首先,需要展示对于某个规模的输入,性质成立;其次,需要展示在此基础上,性质也成立于规模更大的输入。
-
归纳结论(Inductive Conclusion):通过基本情况、归纳假设和归纳步骤,得出结论:根据递归定义或算法的性质,性质对于所有可能的输入都成立。
这个证明过程与一般的归纳证明相似,但由于递归的特殊性质,需要特别关注基本情况、归纳假设和归纳步骤的设计。递归的归纳证明能够展示出递归定义或算法的正确性,并加深对递归过程的理解。
需要注意的是,递归的归纳证明并不总是适用于所有的递归定义或算法。有时,其他的证明技巧可能更合适。此外,在进行递归的归纳证明时,正确定义和选择递归的终止条件也非常重要。
1 #include <iostream> 2 #include <vector> 3 4 // 交换数组中两个位置的元素 5 void swap(int& a, int& b) { 6 int temp = a; 7 a = b; 8 b = temp; 9 } 10 11 // 递归生成数组的排列 12 void generatePermutations(std::vector<int>& arr, int start, int end) { 13 if (start == end) { 14 // 输出排列结果 15 for (int i = 0; i <= end; i++) { 16 std::cout << arr[i] << " "; 17 } 18 std::cout << std::endl; 19 } else { 20 // 递归生成排列 21 for (int i = start; i <= end; i++) { 22 swap(arr[start], arr[i]); // 交换元素 23 generatePermutations(arr, start + 1, end); // 递归生成子排列 24 swap(arr[start], arr[i]); // 恢复元素的原始顺序 25 } 26 } 27 } 28 29 int main() { 30 std::vector<int> arr = {1, 2, 3}; // 输入数组 31 int n = arr.size(); 32 33 generatePermutations(arr, 0, n - 1); 34 35 return 0; 36 }
在上述代码中,generatePermutations
函数是一个递归函数,用于生成数组的排列。它采用数组、起始索引和结束索引作为参数。当起始索引等于结束索引时,表示已经生成了一个完整的排列,将其输出。否则,它遍历从起始索引到结束索引的范围,每次选择一个元素作为当前位置,并递归生成子排列。
在 main
函数中,我们定义了一个包含元素 {1, 2, 3}
的数组,并调用 generatePermutations
函数来生成该数组的所有排列。
运行程序,将输出以下结果:
1 1 2 3 2 1 3 2 3 2 1 3 4 2 3 1 5 3 2 1 6 3 1 2
方法还有很多
如迭代的方法,以下是一个使用迭代的排列算法示例:
1 #include <iostream> 2 #include <vector> 3 #include <algorithm> 4 5 void generatePermutations(std::vector<int>& arr) { 6 int n = arr.size(); 7 8 // 用于存储当前排列的索引数组 9 std::vector<int> indices(n); 10 for (int i = 0; i < n; i++) { 11 indices[i] = i; 12 } 13 14 while (true) { 15 // 输出当前排列 16 for (int i = 0; i < n; i++) { 17 std::cout << arr[indices[i]] << " "; 18 } 19 std::cout << std::endl; 20 21 // 查找下一个排列 22 int i = n - 2; 23 while (i >= 0 && indices[i] > indices[i + 1]) { 24 i--; 25 } 26 if (i < 0) { 27 break; // 已经是最后一个排列,退出循环 28 } 29 30 int j = n - 1; 31 while (indices[j] < indices[i]) { 32 j--; 33 } 34 35 // 交换元素 36 std::swap(indices[i], indices[j]); 37 38 // 反转后续元素 39 std::reverse(indices.begin() + i + 1, indices.end()); 40 } 41 } 42 43 int main() { 44 std::vector<int> arr = {1, 2, 3}; // 输入数组 45 46 generatePermutations(arr); 47 48 return 0; 49 }
更简便的还有字典序算法生成排列的示例。这种方法通过按照字典序生成排列,而不是通过递归或迭代的方式。
1 #include <iostream> 2 #include <algorithm> 3 #include <vector> 4 5 void generatePermutations(std::vector<int>& arr) { 6 std::sort(arr.begin(), arr.end()); // 将数组按升序排列 7 8 do { 9 // 输出当前排列 10 for (int num : arr) { 11 std::cout << num << " "; 12 } 13 std::cout << std::endl; 14 } while (std::next_permutation(arr.begin(), arr.end())); // 生成下一个排列,直到达到最后一个排列 15 } 16 17 int main() { 18 std::vector<int> arr = {1, 2, 3}; // 输入数组 19 20 generatePermutations(arr); 21 22 return 0; 23 }
包括可以使用标准库中的 std::next_permutation
函数来生成数组的所有排列
1 #include <iostream> 2 #include <algorithm> 3 #include <vector> 4 5 void generatePermutations(std::vector<int>& arr) { 6 std::sort(arr.begin(), arr.end()); // 将数组按升序排列 7 8 do { 9 // 输出当前排列 10 for (int num : arr) { 11 std::cout << num << " "; 12 } 13 std::cout << std::endl; 14 } while (std::next_permutation(arr.begin(), arr.end())); 15 } 16 17 int main() { 18 std::vector<int> arr = {1, 2, 3}; // 输入数组 19 20 generatePermutations(arr); 21 22 return 0; 23 }
方法还有许多许多
今天就到这里吧 润!