写完了代码第一件事是编译,编译失败只能根据编译器通知修改代码;编译通过了第二件事是跑UT,UT不过需要进行调试。调试包括debug和release包的调试,线上进程的运行问题有时候也需要调试,调试的主要方式是调试工具和日志(包括print大法)。为了发现问题,有时候还需要添加报警日志。
提高代码的健壮性,编译器、静态检查和格式化工具、调试、单元测试、日志等是开发必不可少的
C语言和C++
编译
为什么需要编译?
- 计算方面,汇编代码基本可以等同于机器码。汇编语言和机器码是一行一行指令执行,高级语言的会抽象出来if-else条件执行/while循环执行和函数执行模块,编译需要把这些模块转化成一行一行的指令;不同cpu提供的指令和寄存器不同,因而编译器的目标指令也不同(其他硬件需要和cpu兼容,一般程序只需要操纵cpu就可以同时操纵内存、磁盘、网卡等硬件);另外,由操作系统实现中断、上下文切换等计算单元(不需要用户程序实现),也需要编译器把它们打包到编译后的二进制文件中。
- 数据方面,汇编语言没有类型概念,需要手动指定寄存器和内存物理地址传输数据,数据交换一般是cpu字长对齐的(64位cpu8字节对齐);高级语言使用类型来确定数据内存分配大小,使用变量维护某一块内存区域,需要编译器记录类型变量的地址,使用变量转化为从某寄存器或内存地址拿数据的指令,动态内存转化为执行从空闲内存拿内存的指令。
编译把.cpp 转化为 .o文件。对于多.cpp文件或使用到动态/静态库的文件(几乎所有.cpp都会使用到库文件,例如glibc),需要使用链接把多个.o文件链接成一个二进制文件。链接期间还会链接跨文件共享的外部变量/全局变量。
C语言和C++一般公用编译器,常用的编译器有两个,gcc和clang/llvm。常见的gcc编译指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| # 编译指令 g++ -c file.cpp # 只编译 g++ file.cpp -o program # 编译链接 g++ file1.cpp file2.cpp -o program # 多文件编译
# 编译优化选项 -O0 # 不进行优化(默认)。 -O1 # 基础优化,平衡编译速度和运行效率。 -O2 # 更高的优化级别,提高性能。 -O3 # 最高优化级别,可能增加编译时间和可执行文件体积。 -Os # 优化以减小可执行文件的大小。
# 调试选项 -g # 生成调试信息,用于调试器(如 gdb)。 -ggdb # 为 GNU 调试器生成更详细的调试信息。
# C++ 标准选项 -std=c++98 # 使用 C++98 标准。 -std=c++11 # 使用 C++11 标准。 -std=c++14 # 使用 C++14 标准。 -std=c++17 # 使用 C++17 标准。 -std=c++20 # 使用 C++20 标准。
# 警告选项 -Wall # 启用大多数常见的警告。 -Wextra # 启用额外的警告。 -Werror # 将警告视为错误。 -pedantic # 强制严格遵循标准。
# 链接选项 -l # 指定链接库。 -L # 指定库文件搜索路径。 -I # 指定头文件搜索路径。 g++ file.cpp -o program -lm -L/usr/lib -I/usr/include
# 生成汇编代码 g++ -S file.cpp -o file.s
# 定义宏 g++ -DDEBUG file.cpp -o program
# 生成动态库 g++ -fPIC -c file1.cpp file2.cpp # -fPIC 生成位置无关代码 g++ -shared -o libmylib.so file1.o file2.o # -shared 指定生成动态库。 g++ main.cpp -L. -lmylib -o program
|
C/C++可以使用Makefile/CMake来组织编译,例如makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| # 变量定义 CC = g++ CFLAGS = -Wall -g OBJ = main.o utils.o TARGET = program
# 规则 $(TARGET): $(OBJ) $(CC) $(CFLAGS) -o $@ $^
main.o: main.cpp utils.h $(CC) $(CFLAGS) -c $<
utils.o: utils.cpp utils.h $(CC) $(CFLAGS) -c $<
# 伪目标 clean: rm -rf $(OBJ) $(TARGET)
# $@ 当前目标的名字。 # $< 第一个依赖文件。 # $^ 所有依赖文件。
make # 构建目标 make clean # 清理构建文件
|
CMake能自动生成 Makefile 或其他构建系统文件(如 Ninja 或 Visual Studio 项目文件)
makefile和cmake语法都比较复杂, google提供的ninjia和bazel 相对简单的构建工具.
cmake 构建语法
基本命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| # CMake 最低版本要求 cmake_minimum_required(VERSION 3.10)
# 定义项目名称和语言 project(MyProject VERSION 1.0 LANGUAGES C CXX)
# add_executable 添加可执行文件 add_executable(myapp main.cpp)
# add_library 添加库 add_library(mylibrary STATIC lib.cpp)
# target_link_libraries 将库链接到目标 target_link_libraries(myapp mylibrary)
# include_directories为编译器添加头文件搜索路径。 include_directories(${CMAKE_SOURCE_DIR}/include)
# add_dependencies 控制目标之间的构建顺序,构建顺序 # add_dependencies(<target> <depend1> <depend2> ...)
# set 设置变量值 set(MY_VAR "Hello")
# message 打印信息,支持不同的输出级别 message(STATUS "This is a status message.") message(WARNING "This is a warning message.") message(ERROR "This is an error message.")
# if()判断条件 if(MY_VAR STREQUAL "Hello") message(STATUS "Hello") elseif(MY_VAR STREQUAL "World") message(STATUS "World") else() message(STATUS "Something else") endif()
# find_package 查找外部库或软件包 find_package(OpenGL REQUIRED)
# find_program 搜索指定的可执行程序,并将其路径存储到指定的变量中。HINTS优先查找的路径 set(BUSTUB_CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/usr/local/opt/llvm/bin") find_program(CLANG_FORMAT_BIN NAMES clang-format clang-format-14 HINTS ${BUSTUB_CLANG_SEARCH_PATH})
# option 定义布尔选项,通常用于启用或禁用功能 option(MY_FEATURE "Enable MyFeature" ON)
# install 指定安装规则 install(TARGETS myapp DESTINATION bin)
# CMake 在父目录中执行 add_subdirectory() 时,CMake 会进入子目录 <source_dir>,并寻找子目录中的 CMakeLists.txt 文件 add_subdirectory()
# cmake常用变量 CMAKE_SOURCE_DIR # 项目源代码的根目录 CMAKE_BINARY_DIR # 构建目录 CMAKE_CURRENT_SOURCE_DIR # 当前 CMake 脚本所在目录。 CMAKE_CURRENT_BINARY_DIR # 当前 CMake 构建目录。 CMAKE_CXX_COMPILER # C++ 编译器。 CMAKE_BUILD_TYPE # 构建类型(如 Debug, Release)
|
cmake -DCMAKE_BUILD_TYPE=Debug ..
命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| STREQUAL # STREQUAL 用于比较两个字符串是否相等,区分大小写
EXISTS # 逻辑判断命令,用于检查某个文件或目录是否存在
file # file(<operation> <arguments>...) # file(READ "<file_path>" <variable_name>) 读取文件内容,保存到变量 # file(TO_CMAKE_PATH) 将输入路径转换为 CMake 使用的标准路径格式
enable_testing() # 启用 CTest 功能,CMake 脚本中使用 add_test() 来定义测试用例,并通过 ctest 命令执行测试。
string() # string(<COMMAND> <ARGUMENTS>) 字符串处理函数 # string(CONCAT <VAR> <STRING1> <STRING2> ...) 将多个字符串连接成一个字符串,并将结果存储到变量
add_custom_target # 运行命令,例如执行clang-tidy add_custom_target(format ${BUSTUB_BUILD_SUPPORT_DIR}/run_clang_format.py ${CLANG_FORMAT_BIN} ${BUSTUB_BUILD_SUPPORT_DIR}/clang_format_exclusions.txt --source_dirs ${BUSTUB_FORMAT_DIRS} --fix --quiet )
gtest_discover_tests # gtest_discover_tests 是 CMake 中与gtest集成的测试自动发现和注册 # 使用 CMake 查找 Google Test find_package(GTest REQUIRED)
# 添加 Google Test 测试目标 add_executable(my_test_target test_case1.cpp test_case2.cpp) target_link_libraries(my_test_target GTest::GTest GTest::GMock)
# 使用 gtest_discover_tests 自动发现测试 gtest_discover_tests(my_test_target)
|
生成表达式,生成表达式用 $<> 来表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| # $<TARGET_OBJECTS:target> 用于获取一个目标的所有对象文件,常用于链接库 add_library( bustub_recovery OBJECT log_manager.cpp)
set(ALL_OBJECT_FILES ${ALL_OBJECT_FILES} $<TARGET_OBJECTS:bustub_recovery> PARENT_SCOPE) # PARENT_SCOPE,将变量值传到父作用域
# $<BUILD_INTERFACE:path> 指定在构建时使用的路径或选项,通常与 $<INSTALL_INTERFACE> 配合使用,指定在安装时使用的路径。 target_include_directories(mylib PUBLIC $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include> $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include> )
|
单元测试
google 的gtest库是广泛使用的单元测试框架,本地mock可以使用gmock。
静态检查和格式化
Clang-Tidy 是一个基于 Clang 的 C++ 静态分析工具,用于执行代码检查、风格检测和代码优化。Clang-Tidy配置文件通常位于项目的根目录,名为 .clang-tidy
1 2 3 4
| Checks: '*, -clang-analyzer-*' WarningsAsErrors: 'true' HeaderFilterRegex: '.*' FormatStyle: file
|
Clang-Format 用于格式化 C++ 代码,并且支持根据 .clang-format 配置文件自定义格式化规则。
1 2 3 4
| clang-format -i <your-file.cpp>
# 指定风格 clang-format -i -style=google <file>
|
Valgrind 内存动态检查工具,可以检查内存泄漏、内存泄漏,未初始化内存访问等
1 2
| # 检查内存泄漏 valgrind --leak-check=full ./your_program
|
自动补全
使用clangd实现自动补全,命令行安装sudo apt-get install clangd-10
cmake启用CMAKE_EXPORT_COMPILE_COMMANDS,会在build目录生成compile_commands.json 文件
日志
编码使用的printf 也是一种简单的日志调试,根据输出看预期是否正确。C++日志库也可以选择google的glog。
调试工具
gdb调试普通程序,需要对普通程序编译加-g选项,也就是debug编译。
gdb调试命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| # 运行调试器 run run arg1 arg2
# 设置断点 break main break 10 break 10 break 10 if x == 5 # 条件断点
# 删除断点 delete delete 1
# 显示断点 info breakpoints info breakpoint 1
# 启用和关闭断点 enable 2 disable 2
# 断点继续执行 continue
# 单步执行 step # 进入函数内部执行 next # 会跳过函数调用 finish # 运行直到当前函数执行结束
# 查看线程堆栈 backtrace frame 1 # 切换栈 up # 切换到当前栈帧的上一层,即父函数的栈帧 down # 切换到当前栈帧的下一层,即子函数的栈帧
# 查看变量值 print x print my_struct.field print my_array[2]
# 修改变量值 set variable x = 10
# 查看指定内存地址内容 x/4xw &x
|
gdb多线程调试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| # 显示线程 info threads
# 切换线程 thread 2
# 查看线程栈 backtrace
# 打印所有线程堆栈 thread apply all backtrace
# 打印所有线程堆栈到文件 set logging on set logging file <filename> # 执行这两行命令,后面gdb会把结果输出到文件 info threads thread apply all backtrace
|
对于release 编译的调试,需要等进程运行产生core后,结合core对二进制文件进行调试。调试命令gdb <二进制文件> <core_file>
,core文件和二进制文件必须对应,一般来说需要保证core是调试的二进制文件生成的。
配置linux系统生成core文件
1 2 3 4
| ulimit -c ulimit -c unlimited # 设置将 core 文件保存在 /tmp/ 目录,并包括程序的名称 (%e) 和进程 ID (%p) 作为文件名。 echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
|
core文件的调试只能查看进程崩溃时的状态,以下是主要使用的命令
1 2 3 4 5 6 7 8 9 10 11
| // 查看进程崩溃时的函数调用栈 backtrace // 查看当前线程的栈帧和寄存器状态 info locals info registers
# 打印所有线程堆栈到文件 set logging on set logging file <filename> # 执行这两行命令,后面gdb会把结果输出到文件 info threads thread apply all backtrace
|
gdb 可以通过gdb attach pid
附加到进程,执行命令后,gdb会附加到目标进程,并暂停目标进程的执行,不要在线上使用!
进程运行时调试工具
pstack 打印正在运行进程的堆栈信息,strace打印系统调用信息。多次执行pstack和strace可以得到执行慢的调用栈、系统调用等
perf 是linux内核提供的性能排查工具。可进行函数级和指令级的性能瓶颈查找。
JAVA
JAVA编译和运行
Java编译是将.java 文件转换为字节码(.class 文件),字节码并不是cpu可执行的汇编机器码,平台无关。编译使用java提供的编译器javac执行。
编译后,每个java文件都会产生一个.class文件,类似C++的.o文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| # 编译.java文件 javac HelloWorld.java javac Class1.java Class2.java javac *.java
# -classpath指定编译查找的类路径。 javac -classpath /path/to/library.jar MyProgram.java javac -classpath /path/to/library1.jar:/path/to/library2.jar MyProgram.java
# -d 指定编译输出.class 文件的目录 javac -d bin MyProgram.java
# -g添加调试信息 javac -g MyProgram.java
|
运行.class文件,只需要显示运行携带main方法的类,相关的类会自动被加载
1 2
| # 不需要加.class后缀 java HelloWorld
|
调试
jdb是java提供的调试工具, jdb比较反人类的是,它的命令没有简写,例如next不能写作n, cont不能写作c, 以及反人类的命令stop at MyProgram:10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| # 开始运行 run # 查看帮助信息 help
# 设置断点 stop at MyProgram:10 stop in MyProgram.myMethod
Usage: stop at <class>:<line_number> or stop in <class>.<method_name>[(argument_type,...)]
# 查看断点(没错clear是查看断点) clear # 删除某个断点,不支持删除全部断点。 clear MyProgram:10
# 断点继续运行 cont
# 单步执行 next step
# 返回到上层调用,类似gdb的finish step up
list # 显示旁边代码
# 查看变量 print variableName print objectInstance.memberVariable
# 查看线程信息 thread # 当前线程信息 thread 1 # 1号线程信息 # 暂停和恢复某线程的运行 suspend [thread id(s)] resume [thread id(s)]
# 查看线程的栈帧 where # 当前线程信息 where 2
# 查看class和method信息 class <className> method <>
|
调试正在运行的进程
1 2 3 4 5
| # 列出正在运行的java进程 jps -l
# 连接正在运行的进程 jdb -attach <pid>
|
测试
JUnit 是最常用的 Java 单元测试框架,使用注解 @Test 来标记测试方法,以及 @Before 和 @After 来标记测试前后的初始化和清理方法。
Go
编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| # 编译单个文件 go build main.go
# 编译整个包 go build
# 编译参数, -gcflags go build -gcflags "-N" main.go
-N # 禁用优化 -l # 禁用内联优化 -l -N # 禁用优化和内联 -m # 输出优化决策信息 -d # 增加调试信息
|
调试
golang推荐使用Delve 进行调试,安装go install github.com/go-delve/delve/cmd/dlv@latest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| # 调试单个文件 dlv debug main.go # 调试某个目录 dlv debug
# 设置断点 break b b main.go:10 # 显示已经设置的断点 breakpoints (alias: bp)
# 栈帧移动 down ------------------------ Move the current frame down. up -------------------------- Move the current frame up.
# 继续运行直到下一个断点 continue c
# 单步执行 next n # 单步执行,跳过函数调用 step s # 单步进入函数内部
# 打印变量值 print p p x
# 显示当前代码行及上下文 list 或 ls
goroutine ------------------- Shows or changes current goroutine goroutines # 查看当前所有 Goroutines # 查看goroutine堆栈 stack <goroutine_id>
threads 显示所有线程信息 thread (alias: tr) ---------- Switch to the specified thread.
|
静态代码检查
gopls,通常会随 Go 扩展自动安装。可以和vscode结合配置
1 2 3 4 5 6 7 8 9 10 11
| # 静态检查 gopls check <path-to-your-directory-or-file> # 格式化代码 gopls format <path-to-your-file> # 代码补全 gopls completion <path-to-your-file>:<line>:<column>
# 跳转到函数定义 gopls definition <path-to-your-file>:<line>:<column> # 跳转到引用 gopls references <path-to-your-file>:<line>:<column>
|
单元测试
Go 自带的测试框架(testing 包)支持单元测试和性能测试。测试文件以 _test.go 结尾, 测试函数必须以 Test 开头,后面跟随被测试的内容的名称。
1 2 3 4 5 6 7 8 9 10
| # 运行测试 go test go test -run TestAdd # 只运行特定的测试函数 go test -cover # 查看覆盖率
# 调试测试 dlv test
# 调试单个测试 dlv test -- -test.run TestFunctionName
|
Python
编译和运行
Python 会将源代码编译为一种字节码(Bytecode),存储为 .pyc 文件(位于 __pycache__
文件夹)。字节码会被 Python 虚拟机(PVM,Python Virtual Machine)翻译为底层的机器指令执行。
相比java通常把字节码打包成jar,后续由jvm执行; python一般直接保留源代码,python虚拟机直接执行源代码。python编译过程也不会进行类型检查,编译器优化行为少。
python语法比较灵活,表达式函数可以在全局执行,执行实现先于main模块。
调试
python -m pdb your_program.py 启用pdb调试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| # 单步运行 n next # n s step # 进入当前行中的函数调用,逐步执行 c continue # 继续执行程序,直到下一个断点 r return # 运行到当前函数完毕
# 清理断点 clear clear filename:lineno # 删除某行所有断点 clear number # 删除编号断点
enable bpnumber # 启动和关闭断点 disable bpnumber
# 打印变量值或表达式 p <expression>
# 列出当前行附近的代码 l (list)
# 设置断点 b break # 显示已经设置的断点 b <line_number> b 12 b add
# 显示当前的调用栈 w (where) up # 切换到调用栈的上一层(即父函数) down # 切换到调用栈的下一层(即子函数)
<expression> # 执行表达式,可以修改变量 a = 10
|
静态检查
pylint 工具检查代码,执行
1 2 3 4 5
| pylint <your-python-file.py> pylint <your-project-folder>
# 生成pylint的静态检查规则 pylint --generate-rcfile > .pylintrc
|
pylint不支持自动格式化,Black可以用来做python自动化代码格式化工具
单元测试
unittest 库,导入待测试的模块,对需要测试的模块进行单元测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import unittest import math_utils
class TestMathUtils(unittest.TestCase): def test_add(self): self.assertEqual(math_utils.add(1, 2), 3) self.assertEqual(math_utils.add(-1, 1), 0) self.assertEqual(math_utils.add(0, 0), 0)
def test_subtract(self): self.assertEqual(math_utils.subtract(10, 5), 5) self.assertEqual(math_utils.subtract(0, 1), -1) self.assertEqual(math_utils.subtract(100, 50), 50)
if __name__ == "__main__": unittest.main()
|
日志
配置日志处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import logging
logger = logging.getLogger("my_logger") logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler() console_handler.setLevel(logging.DEBUG)
file_handler = logging.FileHandler("app.log") file_handler.setLevel(logging.WARNING)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") console_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
logger.addHandler(console_handler) logger.addHandler(file_handler)
logger.debug("这是一条调试日志") logger.info("这是一条一般信息日志") logger.warning("这是一条警告日志") logger.error("这是一条错误日志") logger.critical("这是一条严重错误日志")
|