每日计划-1109
1. 完成 104. 二叉树的最大深度
class Solution {
public:
// 计算二叉树的最大深度的函数
int maxDepth(TreeNode* root) {
// 如果根节点为空,说明已经到达叶子节点的下一层,返回0(这里代码中 return false 应该是错误的,应该是 return 0)
if (!root) return 0;
// 递归计算左子树的最大深度
int L = maxDepth(root->left);
// 递归计算右子树的最大深度
int R = maxDepth(root->right);
// 返回左右子树中较大的深度加1(因为当前节点本身也算一层)
return max(L, R) + 1;
}
};
2. 八股部分
1) C/C++ 中的预处理器指令有哪些?举例说明其用途。
#include
指令- 用途:用于将指定的头文件内容包含到当前源文件中。头文件中通常包含函数声明、变量声明、宏定义、结构体定义等内容,通过
#include
可以方便地在多个源文件中共享这些定义,避免重复编写代码。 - 示例:
- 例如,在一个 C++ 程序中,如果要使用输入输出流(
iostream
)相关的功能,需要在源文件开头包含<iostream>
头文件:
- 例如,在一个 C++ 程序中,如果要使用输入输出流(
- 用途:用于将指定的头文件内容包含到当前源文件中。头文件中通常包含函数声明、变量声明、宏定义、结构体定义等内容,通过
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
这里的#include <iostream>
将iostream
头文件中的内容引入到当前源文件,使得可以使用cout
和endl
等对象进行输出操作。如果不包含这个头文件,编译器将无法识别cout
和endl
等符号。
2. #define
指令
- 用途:用于定义宏。宏可以是常量的别名,也可以是一段代码的缩写,在预编译阶段会将代码中出现的宏名替换为其定义的内容。
- 示例:
- 定义常量宏:
#define PI 3.14159
int main() {
double radius = 5.0;
double area = PI * radius * radius;
std::cout << "圆的面积为:" << area << std::endl;
return 0;
}
这里的#define PI 3.14159
定义了一个常量宏PI
,在代码中使用PI
时,预处理器会将其替换为3.14159
。
#define MAX(a, b) ((a) > (b)? (a) : (b))
int main() {
int x = 10, y = 20;
int max_value = MAX(x, y);
std::cout << "最大值为:" << max_value << std::endl;
return 0;
}
这里的#define MAX(a, b) ((a) > (b)? (a) : (b))
定义了一个宏MAX
,它接受两个参数a
和b
,并返回较大的值。在代码中使用MAX(x, y)
时,预处理器会将其展开为((x) > (y)? (x) : (y))
。
3. #ifdef
、#ifndef
、#endif
指令
- 用途:用于条件编译,根据条件决定是否编译某段代码。
#ifdef
用于判断某个宏是否已定义,#ifndef
用于判断某个宏是否未定义,#endif
用于结束条件编译块。 - 示例:
- 例如,假设有一个调试宏
DEBUG
,在调试时希望输出一些调试信息,而在正式发布时不希望包含这些调试信息,可以这样使用条件编译:
- 例如,假设有一个调试宏
- 定义带参数的宏(类似函数的功能,但有区别,后面会详细解释):
#define DEBUG
int main() {
int value = 10;
#ifdef DEBUG
std::cout << "调试信息:当前值为 " << value << std::endl;
#endif
return 0;
}
当定义了DEBUG
宏时(如上述代码所示),#ifdef DEBUG
和#endif
之间的代码会被编译,输出调试信息。如果没有定义DEBUG
宏,这段代码将不会被编译,不会生成额外的调试输出代码,从而减小可执行文件的大小,提高程序的执行效率。
- 使用
#ifndef
类似,例如防止头文件重复包含:
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 头文件的内容,如结构体定义、函数声明等
#endif
第一次包含这个头文件时,MY_HEADER_H
未定义,#ifndef MY_HEADER_H
条件成立,会定义MY_HEADER_H
并编译头文件内容。后续再次包含时,由于MY_HEADER_H
已定义,#ifndef MY_HEADER_H
条件不成立,头文件内容不会被重复编译,避免了重复定义错误。
4. #undef
指令
- 用途:用于取消已定义的宏。
- 示例
#define FOO 42
int main() {
std::cout << "FOO的值为:" << FOO << std::endl;
#undef FOO
// 这里再次使用FOO会导致编译错误,因为它已被取消定义
// std::cout << "取消定义后FOO的值为:" << FOO << std::endl;
return 0;
}
在上述代码中,先定义了宏FOO
,然后使用#undef FOO
取消了它的定义。取消定义后,如果再尝试使用FOO
,编译器会报错,除非在后续代码中重新定义它。
5. #pragma
指令
- 用途:这是一个特定于编译器的指令,用于向编译器提供额外的信息或控制编译器的行为。不同的编译器对
#pragma
指令的支持和用法可能不同。 - 示例:
- 例如,在一些编译器中,可以使用
#pragma once
来确保头文件只被编译一次,类似于前面提到的#ifndef
、#define
、#endif
的组合,但更简洁(不过不是所有编译器都支持#pragma once
):
- 例如,在一些编译器中,可以使用
#pragma once
// 头文件的内容
还有一些编译器可以使用#pragma warning
来控制警告信息的显示或忽略,例如:
#pragma warning(disable : 4996) // 禁用特定的警告(这里假设4996是某个编译器的警告编号,用于举例)
int main() {
// 可能会产生警告的代码
return 0;
}
2) 解释一下宏定义和函数调用的区别。
- 执行时机不同
- 宏定义:在预编译阶段进行文本替换,也就是在编译程序之前,将代码中所有出现的宏名替换为其定义的内容。例如,对于宏
#define MAX(a, b) ((a) > (b)? (a) : (b))
,在预编译时会直接将MAX(x, y)
替换为((x) > (y)? (x) : (y))
,然后再进行编译。 - 函数调用:在程序运行时,当执行到函数调用语句时才会跳转到函数定义处执行函数体中的代码,执行完函数后再返回到调用点继续执行后续代码。
- 宏定义:在预编译阶段进行文本替换,也就是在编译程序之前,将代码中所有出现的宏名替换为其定义的内容。例如,对于宏
- 参数处理方式不同
- 宏定义:宏定义中的参数只是简单的文本替换,没有类型检查。例如,
MAX(1, 2)
和MAX(1.0, 2.0)
都会按照宏定义的规则进行替换,但如果宏定义中的参数在替换后出现语法错误(如MAX(1, "hello")
,替换后可能会导致比较操作不合法),编译器可能无法在预编译阶段检测到,直到编译阶段才可能报错,而且错误信息可能不太直观,因为是基于替换后的代码报错。 - 函数调用:函数调用时会对参数进行类型检查,确保传递的参数类型与函数定义中的参数类型匹配。如果类型不匹配,编译器会在编译阶段报错,并且错误信息通常会明确指出参数类型不匹配的问题。例如,定义了一个函数
int add(int a, int b)
,如果调用add(1, 2.0)
,编译器会提示参数类型不匹配的错误。
- 宏定义:宏定义中的参数只是简单的文本替换,没有类型检查。例如,
- 返回值处理不同
- 宏定义:宏本身没有返回值的概念,它只是进行文本替换。虽然可以通过宏定义实现类似返回值的效果(如上面
MAX
宏通过三元表达式返回较大值),但本质上还是文本替换后的表达式计算结果。 - 函数调用:函数有明确的返回值类型,函数体中的
return
语句用于返回一个值,这个值的类型必须与函数声明的返回值类型兼容。函数调用可以将返回值赋给变量或用于其他表达式的计算。例如,int result = add(1, 2);
,函数add
返回计算结果,然后赋值给result
变量。
- 宏定义:宏本身没有返回值的概念,它只是进行文本替换。虽然可以通过宏定义实现类似返回值的效果(如上面
- 代码大小和执行效率方面的差异(在简单情况下)
- 宏定义:如果宏定义在代码中多次使用,每次使用都会进行文本替换,可能会导致代码膨胀,生成的可执行文件可能会更大。但由于是在预编译阶段进行替换,没有函数调用的开销(如函数调用时的参数压栈、栈帧创建与销毁、返回地址保存与恢复等操作),在某些情况下执行效率可能会更高,尤其是对于简单的、频繁调用的宏(如简单的数学计算宏)。
- 函数调用:函数代码只存在一份,无论在代码中调用多少次,不会像宏定义那样导致代码大量重复(除非是内联函数,内联函数会在调用处展开代码,类似宏替换,但内联函数仍然有函数的特性,如类型检查等,并且编译器会根据一定规则决定是否内联展开)。不过函数调用会有一定的开销,对于频繁调用的简单函数,函数调用开销可能会对性能产生一定影响。例如,一个简单的函数用于计算两个整数的和,如果在一个循环中频繁调用这个函数,函数调用的开销可能会比直接使用宏计算和的开销大(但实际情况中,现代编译器会对函数进行优化,如内联优化等,可能会减少这种性能差异)。
- 作用域和生命周期不同
- 宏定义:宏定义的作用域从定义处开始,到文件末尾结束,除非被
#undef
取消定义。宏不存在像函数那样的局部变量和局部作用域概念,它只是在预编译阶段进行文本替换,对整个代码中的文本进行影响。 - 函数调用:函数有自己的作用域,函数内部定义的变量是局部变量,其生命周期在函数调用时开始,函数返回时结束。函数可以在不同的作用域中被调用,并且可以通过参数传递和返回值与其他部分的代码进行交互。例如,在一个函数内部定义的局部变量不会影响到其他函数中的同名变量,而宏定义在整个文件中替换后可能会影响到其他部分代码中相同宏名的使用(如果有重名情况且没有适当的作用域隔离机制,如在不同的函数中使用相同宏名但期望不同行为时可能会出现问题)。
- 宏定义:宏定义的作用域从定义处开始,到文件末尾结束,除非被