C++跨模块访问类中静态成员变量
C++访问跨模块类中静态成员变量
0. 结论
在C++中,无法直接访问跨模块类中的静态变量成员,必须通过静态方法访问。
1. 示例
为了说明此问题,示例源码如下:
1.1 文件结构
文件结构如下图:
1.2 文件内容
1.2.1 工程CMakeLists.txt
工程的CMakeLists.txt(即F:\learn_cmake目录下的):
cmake_minimum_required(VERSION 3.18)
## 设置工程名称
set(PROJECT_NAME KZN)
## 设置工程版本号
set(PROJECT_VERSION "1.0.1.0" CACHE STRING "默认版本号")
## 工程定义
project(${PROJECT_NAME}
LANGUAGES CXX C
VERSION ${PROJECT_VERSION}
)
## 配置默认构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "默认构建类型")
endif()
## 通过空指针字节数来判断运行平台(即64或32位系统)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(${PROJECT_NAME}_PLATFORM_NAME "x64")
else()
set(${PROJECT_NAME}_PLATFORM_NAME "Win32")
endif()
## 打印选项ACCESS_STATIC_MEMBER的值
message(STATUS "ACCESS_STATIC_MEMBER:${ACCESS_STATIC_MEMBER}")
## 若为真,则添加定义源文件的预处理符号
if(ACCESS_STATIC_MEMBER)
## 添加源文件预处理符号ACCESS_STATIC_MEMBER
## 添加源文件预处理符号:控制是否直接访问静态成员变量
## 在代码中将使用此宏
##add_definitions("-DACCESS_STATIC_MEMBER")
endif()
## 打印选项ACCESS_STATIC_FUNCTION的值
message(STATUS "ACCESS_STATIC_FUNCTION:${ACCESS_STATIC_FUNCTION}")
## 若为真,则添加定义源文件的预处理符号
if(ACCESS_STATIC_FUNCTION)
## 添加源文件预处理符号ACCESS_STATIC_FUNCTION
## 作用:控制是否通过访问静态成员函数以访问静态成员变量
## 在代码中将使用此宏
add_definitions("-DACCESS_STATIC_FUNCTION")
endif()
## 禁止RelWithDebInfo的优化
if(MSVC)
## 打印替换前的值
message(STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO_Before_Replace:${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
## 将字符串CMAKE_CXX_FLAGS_RELWITHDEBINFO中的/O2使用/Od进行替换,然后替换后的结果再存入CMAKE_CXX_FLAGS_RELWITHDEBINFO
STRING(REPLACE "/O2" "/Od" CMAKE_CXX_FLAGS_RELWITHDEBINFO ${CMAKE_CXX_FLAGS_RELWITHDEBINFO})
## 打印替换后的值
message(STATUS "CMAKE_CXX_FLAGS_RELWITHDEBINFO_After_Replace:${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
endif()
## 控制VS工程生成使用"文件夹"组织结构
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
## 定义选项:KZN_DEVELOPE_MODE开发模式开关
option(${PROJECT_NAME}_DEVELOPE_MODE "开发模式" ON)
## 开发者模式,统一输出目录,防止本地运行时发生缺少动态库的错误
## 一般都是在根目录CMakeLists.txt中定义,如果子目录中没有指定输出目录,则沿用父目录指定的输出目录
if(${PROJECT_NAME}_DEVELOPE_MODE)
## 开发者模式下统一执行程序的输出目录
if(NOT DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>")
endif()
## 开发者模式下统一动态库的输出目录
if(NOT DEFINED CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>")
endif()
endif()
## 定义工程源代码目录变量
set(PROJECT_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
## 定义运行时输出目录,可以另外自定义目录
#set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/build/KZN/build")
## 打印默认构建类型
message(STATUS "DEFAULT_CMAKE_CONFIGURATION_TYPES:${CMAKE_CONFIGURATION_TYPES}")
if(${PROJECT_NAME}_DEVELOPE_MODE)
## 开发者模式下,配置构建类型列表(注意此处仅仅配置RelWithDebInfo)
set(CMAKE_CONFIGURATION_TYPES RelWithDebInfo)
endif()
## 添加子目录
add_subdirectory(libs) ## 业务模块库
add_subdirectory(src) ## 主程序库
## 设置默认启动工程
set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT KZN2024)
1.2.2 模块库(libs)
1.2.2.1 模块库CMakeLists.txt
模块库的CMakeLists.txt(即F:\learn_cmake\libs目录下的):
add_subdirectory(common)
add_subdirectory(module_a)
1.2.2.2 公共模块(common)
公共模块头文件:
-
kznadd.h
// kznadd.h #pragma once #ifndef KZN_ADD_H #define KZN_ADD_H #include <string> struct KZNInfo { int nType = 0; double dCount = 0.0; std::string sName; }; class KZNAdd { public: // 公共访问权限的静态成员函数 static int add(int a, int b); // 公共访问权限的静态成员函数 static KZNInfo oInfo(); public: // 公共访问权限的静态成员对象 static KZNInfo m_s_oInfo; }; #endif // KZN_ADD_H
-
kznapp.h
// kznapp.h #pragma once #ifndef KZN_APP_H #define KZN_APP_H class KZNApp { public: void exec(int a, int b); }; #endif // KZN_APP_H
公共模块源文件:
-
kznadd.cpp
// kznadd.cpp #include "kznadd.h" // 初始化类中静态成员变量 KZNInfo KZNAdd::m_s_oInfo = KZNInfo{}; int KZNAdd::add(int a, int b) { return (a + b); } KZNInfo KZNAdd::oInfo() { return m_s_oInfo; }
-
kznapp.cpp
// kznapp.cpp #include "kznadd.h" #include "kznapp.h" void KZNApp::exec(int a, int b) { #ifdef ACCESS_STATIC_MEMBER // 此项目内,直接访问类的静态成员对象值:正常 auto oInfoByMember = KZNAdd::m_s_oInfo; #endif #ifdef ACCESS_STATIC_FUNCTION // 此项目内,通过访问类的静态成员函数,读取类的静态成员对象值:正常 auto oInfoByFunc = KZNAdd::oInfo(); #endif }
公共模块的CMakeLists.txt文件:
-
CMakeLists.txt
cmake_minimum_required(VERSION 3.15) ##设置target名称 set(TARGET_NAME KZNCommon) ##设置源代码路径 set(${TARGET_NAME}_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) ##提取当前模块所有源文件(include和src分开提取) file(GLOB_RECURSE ${TARGET_NAME}_HEADER_FILES LIST_DIRECTORIES False CONFIGURE_DEPENDS "${${TARGET_NAME}_SOURCE_DIR}/include/*.h*" ) file(GLOB_RECURSE ${TARGET_NAME}_SRC_FILES LIST_DIRECTORIES False CONFIGURE_DEPENDS "${${TARGET_NAME}_SOURCE_DIR}/src/*.h*" "${${TARGET_NAME}_SOURCE_DIR}/src/*.c*" ) ##为VS设置源代码文件夹 source_group( TREE "${${TARGET_NAME}_SOURCE_DIR}/include" PREFIX "Header Files" FILES ${${TARGET_NAME}_HEADER_FILES} ) source_group( TREE "${${TARGET_NAME}_SOURCE_DIR}/src/" PREFIX "Source Files" FILES ${${TARGET_NAME}_SRC_FILES} ) ##添加target及别名 add_library(${TARGET_NAME} SHARED) ## 动态库 add_library(${PROJECT_NAME}::${TARGET_NAME} ALIAS ${TARGET_NAME}) ## 定义别名 ##指定源文件 target_sources(${TARGET_NAME} PRIVATE ${${TARGET_NAME}_SRC_FILES} PRIVATE ${${TARGET_NAME}_HEADER_FILES} ) ##设置target属性 set_target_properties(${TARGET_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS True ##自动导出符号 FOLDER "libs/common" ##设置VS路径 ) ##设置预处理器定义 if(MSVC) target_compile_definitions(${TARGET_NAME} PRIVATE UNICODE NOMINMAX) ##解决C++17情况下std::byte引起的编译错误(主要是不正确使用std命名空间导致) target_compile_definitions(${TARGET_NAME} PRIVATE "_HAS_STD_BYTE=0") ##解决C++17情况下使用std::auto_ptr引起的编译错误 target_compile_definitions(${TARGET_NAME} PRIVATE "_HAS_AUTO_PTR_ETC=1") endif() ##配置构建/使用时的头文件路径 target_include_directories( ${TARGET_NAME} PUBLIC "$<BUILD_INTERFACE:${${TARGET_NAME}_SOURCE_DIR}/include>" "$<INSTALL_INTERFACE:include>" PRIVATE "$<BUILD_INTERFACE:${${TARGET_NAME}_SOURCE_DIR}/src>" )
1.2.2.3 模块A(module_a)
模块A头文件:
-
kznservice.h
// kznservice.h #pragma once #ifndef KZN_SERVICE_H #define KZN_SERVICE_H class KZNService { public: static void exec(int a, int b); }; #endif // KZN_SERVICE_H
模块A源文件:
-
kznservice.cpp
// kznservice.cpp #include "kznservice.h" #include "kznadd.h" void KZNService::exec(int a, int b) { #ifdef ACCESS_STATIC_MEMBER // 访问依赖项目common中KZNAdd类的静态成员对象值:错误 auto oInfoByMember = KZNAdd::m_s_oInfo; // error:无法解析的外部符号 #endif #ifdef ACCESS_STATIC_FUNCTION // 访问依赖项目common中KZNAdd类的静态成员函数(间接读取类的静态成员对象值):正常 auto oInfoByFunc = KZNAdd::oInfo(); // 访问依赖项目common中KZNAdd类的普通成员函数:正常 auto result = KZNAdd::add(a, b); #endif }
模块A的CMakeLists.txt文件:
-
CMakeLists.txt
cmake_minimum_required(VERSION 3.15) ## 设置target名称 set(TARGET_NAME KZNModuleA) ## 设置源代码路径 set(${TARGET_NAME}_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) ## 提取当前模块所有源文件(include和src分开提取) file(GLOB_RECURSE ${TARGET_NAME}_HEADER_FILES LIST_DIRECTORIES False CONFIGURE_DEPENDS "${${TARGET_NAME}_SOURCE_DIR}/include/*.h*" ) file(GLOB_RECURSE ${TARGET_NAME}_SRC_FILES LIST_DIRECTORIES False CONFIGURE_DEPENDS "${${TARGET_NAME}_SOURCE_DIR}/src/*.h*" "${${TARGET_NAME}_SOURCE_DIR}/src/*.c*" ) ## 为VS设置源代码文件夹 source_group( TREE "${${TARGET_NAME}_SOURCE_DIR}/include" PREFIX "Header Files" FILES ${${TARGET_NAME}_HEADER_FILES} ) source_group( TREE "${${TARGET_NAME}_SOURCE_DIR}/src/" PREFIX "Source Files" FILES ${${TARGET_NAME}_SRC_FILES} ) ## 添加target及定义别名 add_library(${TARGET_NAME} SHARED) ## 动态库 add_library(${PROJECT_NAME}::${TARGET_NAME} ALIAS ${TARGET_NAME}) ## 定义别名 ## 指定target的源文件 target_sources(${TARGET_NAME} PRIVATE ${${TARGET_NAME}_SRC_FILES} PRIVATE ${${TARGET_NAME}_HEADER_FILES} ) ## 设置target属性 set_target_properties(${TARGET_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS True ## 自动导出符号 FOLDER "libs/module_a" ## 设置VS源码文件夹 ) ## 设置预处理器定义 if(MSVC) target_compile_definitions(${TARGET_NAME} PRIVATE UNICODE NOMINMAX) ##解决C++17情况下std::byte引起的编译错误(主要是不正确使用std命名空间导致) target_compile_definitions(${TARGET_NAME} PRIVATE "_HAS_STD_BYTE=0") ##解决C++17情况下使用std::auto_ptr引起的编译错误 target_compile_definitions(${TARGET_NAME} PRIVATE "_HAS_AUTO_PTR_ETC=1") endif() ## 配置构建/使用时的头文件路径 target_include_directories( ${TARGET_NAME} PUBLIC "$<BUILD_INTERFACE:${${TARGET_NAME}_SOURCE_DIR}/include>" "$<INSTALL_INTERFACE:include>" PRIVATE "$<BUILD_INTERFACE:${${TARGET_NAME}_SOURCE_DIR}/src>" ) ## 设置链接依赖库 target_link_libraries(${TARGET_NAME} KZN::KZNCommon)
1.2.3 主程序(src)
1.2.3.1 主程序CMakeLists.txt
主程序CMakeLists.txt,(即F:\learn_cmake\src目录下的):
add_subdirectory(kaizen)
1.2.3.2 主程序文件(kaizen)
主程序头文件:
-
stdafx.h
// stdafx.h : 标准系统包含文件的包含文件, // 或是经常使用但不常更改的 // 特定于项目的包含文件 // #pragma once // Windows 头文件: #include <windows.h> #include <tuple> #include <memory> // std库 #include <set> #include <vector> #include <map> #include <hash_map> #include <hash_set> #include <functional> #include <algorithm> #include <string.h> #include <strstream> #include <assert.h> #include <iostream> #include <memory>
主程序源文件:
-
stdafx.cpp
#include "stdafx.h"
-
main.cpp
#include "stdafx.h" #include "kznadd.h" #include "kznservice.h" int main(int argc, char *argv[]) { #ifdef ACCESS_STATIC_MEMBER // 访问依赖项目common中KZNAdd类的静态成员对象值:错误 auto oInfoByMember = KZNAdd::m_s_oInfo; // error无法解析的外部符号 #endif #ifdef ACCESS_STATIC_FUNCTION // 访问依赖项目common中KZNAdd类的静态成员函数(间接读取依赖项目类的静态成员对象值):正常 auto oInfoByFunc = KZNAdd::oInfo(); #endif return 0; }
主程序的CMakeLists.txt文件:
-
CMakeLists.txt
cmake_minimum_required(VERSION 3.15) ##设置target名称 set(TARGET_NAME KZN2024) ##设置源代码路径 set(${TARGET_NAME}_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) ##提取所有源文件(include与src区分开) file(GLOB_RECURSE ${TARGET_NAME}_HEADER_FILES LIST_DIRECTORIES False CONFIGURE_DEPENDS "${${TARGET_NAME}_SOURCE_DIR}/*.h*" ) file(GLOB_RECURSE ${TARGET_NAME}_SRC_FILES LIST_DIRECTORIES False CONFIGURE_DEPENDS "${${TARGET_NAME}_SOURCE_DIR}/*.h*" "${${TARGET_NAME}_SOURCE_DIR}/*.c*" ) ##为VS设置源代码文件夹 source_group( TREE "${${TARGET_NAME}_SOURCE_DIR}" PREFIX "Header Files" FILES ${${TARGET_NAME}_HEADER_FILES} ) source_group( TREE "${${TARGET_NAME}_SOURCE_DIR}" PREFIX "Source Files" FILES ${${TARGET_NAME}_SRC_FILES} ) ##指定源文件 add_executable(${TARGET_NAME} ${${TARGET_NAME}_SRC_FILES} ${${TARGET_NAME}_HEADER_FILES} ) ## 设置链接器-系统-子系统 ## VS->属性页->链接器->系统->子系统->/SUBSYSTEM:CONSOLE ##set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:CONSOLE") ## /SUBSYSTEM:WINDOWS ##set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") ## /SUBSYSTEM:WINDOWS message(STATUS "CMAKE_EXE_LINKER_FLAGS:${CMAKE_EXE_LINKER_FLAGS}") set_target_properties(${TARGET_NAME} PROPERTIES ## WIN32_EXECUTABLE ON ## 设置Win32可执行程序 LINK_FLAGS "/SAFESEH:NO /LARGEADDRESSAWARE" FOLDER "plugins" ## 设置工程的VS代码路径 ) ##设置库导出 if(MSVC) message(STATUS "KZN_PLATFORM_NAME:${${PROJECT_NAME}_PLATFORM_NAME}") if(${${PROJECT_NAME}_PLATFORM_NAME} STREQUAL "x64") target_compile_definitions(${TARGET_NAME} PRIVATE WIN64) endif() endif() target_compile_definitions(${TARGET_NAME} PRIVATE UNICODE _UNICODE NOMINMAX _SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS) ##配置构建/使用时的头文件路径 target_include_directories(${TARGET_NAME} PRIVATE "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>" ) ##配置库依赖 target_link_libraries(${TARGET_NAME} PRIVATE KZN::KZNCommon KZN::KZNModuleA )
1.2.4 CMake
1.2.4.1 cmake_config_vs_2015.bat
专供VS2015配置使用的CMake脚本:
rem cmake_config_vs_2015.bat
@echo off
set currentDir=%~dp0
echo bat_script_path:%currentDir%
cd ..\
set sourceDir=%cd%
echo source_code_path:%sourceDir%
set cmakeOutputDir=%sourceDir%\build
echo cmake_output_path:%cmakeOutputDir%
cmake -S %sourceDir% -B %cmakeOutputDir% -G"Visual Studio 14 2015" -A x64
pause
1.2.4.2 cmake_config_vs_2019.bat
专供VS2019配置使用的CMake脚本:
rem cmake_config_vs_2019.bat
@echo off
set currentDir=%~dp0
echo bat_script_path:%currentDir%
cd ..\
set sourceDir=%cd%
echo source_code_path:%sourceDir%
set cmakeOutputDir=%sourceDir%\build
echo cmake_output_path:%cmakeOutputDir%
cmake -S %sourceDir% -B %cmakeOutputDir% -G"Visual Studio 16 2019" -T v140 -A x64
pause
1.2.4.3 cmake_config_vs_2022.bat
专供VS2022配置使用的CMake脚本:
rem cmake_config_vs_2022.bat
@echo off
set currentDir=%~dp0
echo bat_script_path:%currentDir%
cd ..\
set sourceDir=%cd%
echo source_code_path:%sourceDir%
set cmakeOutputDir=%sourceDir%\build
echo cmake_output_path:%cmakeOutputDir%
cmake -S %sourceDir% -B %cmakeOutputDir% -G"Visual Studio 17 2022" -T v140 -A x64 -DACCESS_STATIC_FUNCTION=ON -DACCESS_STATIC_MEMBER=OFF
pause
1.3 build目录
根据安装VS的版本,执行对应的bat脚本程序,即可在代码同目录生成build文件夹。
build编译文件与源码文件完全隔离开,防止源码被影响。
比如,本地安装VS2022,执行cmake_config_vs_2022.bat脚本。
生成解决方案sln如下过程:
F:\learn_cmake\cmake>rem cmake_config_vs_2022.bat
bat_script_path:F:\learn_cmake\cmake\
source_code_path:F:\learn_cmake
cmake_output_path:F:\learn_cmake\build
-- Selecting Windows SDK version 10.0.22621.0 to target Windows 10.0.22631.
-- The CXX compiler identification is MSVC 19.0.24247.2
-- The C compiler identification is MSVC 19.0.24247.2
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- ACCESS_STATIC_MEMBER:OFF
-- ACCESS_STATIC_FUNCTION:ON
-- CMAKE_CXX_FLAGS_RELWITHDEBINFO_Before_Replace:/Zi /O2 /Ob1 /DNDEBUG
-- CMAKE_CXX_FLAGS_RELWITHDEBINFO_After_Replace:/Zi /Od /Ob1 /DNDEBUG
-- DEFAULT_CMAKE_CONFIGURATION_TYPES:Debug;Release;MinSizeRel;RelWithDebInfo
-- CMAKE_EXE_LINKER_FLAGS:/machine:x64
-- KZN_PLATFORM_NAME:x64
-- Configuring done (4.9s)
-- Generating done (0.0s)
-- Build files have been written to: F:/learn_cmake/build
请按任意键继续. . .
1.4 解决方案
打开解决方案KZN.sln,如下图示:
2. 解析
-
CMAKE_RUNTIME_OUTPUT_DIRECTORY
CMAKE_RUNTIME_OUTPUT_DIRECTORY:二进制执行文件exe输出目录
CMAKE_LIBRARY_OUTPUT_DIRECTORY:二进制动态库文件dll输出目录
CMAKE_ARCHIVE_OUTPUT_DIRECTORY:静态库文件lib输出目录
以上三者,一般都是在根目录CMakeLists.txt中定义,如果子目录中没有指定输出位置,则沿用父目录指定的输出位置
若不定义CMAKE_RUNTIME_OUTPUT_DIRECTORY,则从target的RUNTIME_OUTPUT_DIRECTORY属性中读取不到内容。
若不定义CMAKE_LIBRARY_OUTPUT_DIRECTORY,则从target的LIBRARY_OUTPUT_DIRECTORY属性中读取不到内容。
-
cmake_config_vs_2022.bat
此脚本中,-DACCESS_STATIC_FUNCTION=ON表示定义选项ACCESS_STATIC_FUNCTION且值为ON,即打开开关。 -DACCESS_STATIC_MEMBER=OFF表示定义选项ACCESS_STATIC_MEMBER且值为OFF,即关闭。
-
理解CMake控制源码预处理符号的逻辑
通过CMake选项ACCESS_STATIC_FUNCTION控制CMakeLists.txt中是否添加定义预处理器符号,从而控制源码的预处理执行逻辑。
3. 总结
默认执行vmake_config_vs_2022.bat脚本,然后使用VS2022打开KZN.sln可以编译通过。
若将-DACCESS_STATIC_MEMBER=ON开关打开后,再执行vmake_config_vs_2022.bat脚本,然后使用VS2022编译解决方案,VS会报错误。根据错误提示内容,可以琢磨理解此篇主题。
即在C++中,无法直接访问跨模块类中的静态变量成员,必须通过静态方法访问。