Stephan Avenwedde 发布的文章

Qt Creator 就是丰富的 Qt 库和程序员之间的粘合剂。

 title=

Qt Creator 是 Qt 框架的默认集成开发环境(IDE),同时也是丰富的 Qt 库和用户之前的粘合剂。除了如智能代码补全、调试、项目管理等基础功能外,Qt Creator 还提供了很多让软件开发变得更简单的特性。

在这篇文章中,我会重点介绍一些我最喜欢的 Qt Creator 特性。

深色模式

当我使用一个新的应用时,我的第一个问题是:它有深色模式吗? Qt Creator 的回答是:你更喜欢哪一种深色模式呢?

你可以在“ 选项 Options ”菜单中激活深色模式。在顶部的菜单栏中,点击“ 工具 Tools ”,选择“ 选项 Options ”,然后转到“ 环境 Environment ”部分。下面是你能选择的常用外观:

 title=

定制外观

像每一个 Qt 应用一样,借助样式表,Qt Creator 的外观是高度可定制化的。下面,你可以按照我的做法给 Qt Creator一个想要的外观。

将下面这些内容写入 mycustomstylesheet.css 文件中:

QMenuBar { background-color: olive }
QMenuBar::item { background-color: olive }
QMenu { background-color : beige; color : black }
QLabel { color: green }

然后使用命令行开启 Qt Creator,将样式表作为参数传入:

qtcreator -stylesheet=mycustomstylesheet.css

IDE 现在看上去应该会变成这样:

 title=

在这份 文档 中可以查阅更多的样式表。

命令行参数

Qt Creator 可接受很多命令行选项。例如,如果想在启动时自动加载当前项目,那么你可以将它的路径传入:

qtcreator ~/MyProject/MyQtProject.pro

你甚至可以将默认应该打开的文件和行数作为参数传递。下面这个命令打开 main.cpp 20 行处:

qtcreator ~/MyProject/main.cpp:20

在这份 文档 中可以查阅更多 Qt 特有的命令行选项。

Qt Creator 和一般的 Qt 应用无二,所以,除了自己的命令行参数以外,它也接收 QApplicationQGuiApplication 的一般参数。

交叉编译

Qt Creator 允许你定义一些被称为“ 配套 Kit ”的工具链。 “配套” 定义了构建和运行应用所需要的二进制库和 SDK。

 title=

这使得你通过两次点击,就在完全不同的工具链之间切换。

 title=

在这份 手册 中可以查阅更多关于配套的内容。

分析工具

Qt Creator 集成了一些最流行的性能分析工具,例如:

 title=

调试器

在调试方面,Qt Creator 为 GNU Debugger(GDB)配备了一个很好的界面。我喜欢它检查容器类型和创建条件断点的方式,很简单。

 title=

FakeVim

如果你喜欢 Vim,你可以在设置中开启 FakeVim,来像 Vim 一样控制 Qt Creator。点击“ 工具 Tools ”,选择“ 选项 Options ”。在 “FakeVim” 选项中,你可以找到许多开关来定制 FakeVim。除了编辑器的功能外,你可以将自己设置的功能和命令关联起来,定制 Vim 命令。

举个例子,你可以将“ 构建项目 Build Project ”的功能和 build 命令关联到一起:

 title=

回到编辑器中,当你按下冒号(:)并输入 build,Qt Creator 利用配置的工具链,开始进行构建:

 title=

你可以在这份 文档 中找到 FakeVim 的更多信息。

类检测器

当使用 C++ 开发时,点击 Qt Creator 右下角的按钮可打开右边的窗口。然后在窗口顶部拉下的菜单中选择“ 大纲 Outline ”。如果你在左侧窗体中有头文件打开,你可以很好地纵览定义的类和类型。如果你切换到源文件中(*.cpp),右侧窗体会列出所有定义的方法,双击其中一个,你可以跳转到这个方法:

 title=

项目配置

Qt Creator 的项目建立在项目目录里的 *.pro-file 之上。你可以为你的项目在 *.pro-file 中添加定制的配置。我向 *.pro-file 中添加了 my_special_config,它向编译器的定义添加 MY_SPECIAL_CONFIG

QT -= gui

CONFIG += c++11 console
CONFIG -= app_bundle

CONFIG += my_special_config

my_special_config {
DEFINES += MY_SPECIAL_CONFIG
}

Qt Creator 自动根据当前配置设置代码高亮:

 title=

*.pro-file 使用 qmake 语言 进行编写。

总结

这些特性仅仅是 Qt Creators 所提供的特性的冰山一角。初学者们应该不会感到被其众多的功能所淹没,Qt Creator 是一款对初学者很友好的 IDE。它甚至可能是入门 C++ 开发最简单的方式。如果要获得 QT Creator 特性的全面概述,请参考它的 官方文档

(插图来自 Stephan Avenwedde, CC BY-SA 4.0


via: https://opensource.com/article/21/6/qtcreator

作者:Stephan Avenwedde 选题:lujun9972 译者:hadisi1993 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用 Samba 设置跨平台文件共享。

 title=

如果你使用不同的操作系统,能够在它们之间共享文件会让你倍感方便。这篇文章介绍如何使用 Sambamount.cifs 在 Linux (Fedora 33)和 Windows 10 之间设置文件共享。

Samba 是 SMB/CIFS 协议的 Linux 实现,允许通过网络连接直接访问共享的文件夹和打印机。 mount.cifs 是 Samba 套件的一部分,可让你在 Linux 下挂载 CIFS 文件系统。

注意: 这些说明适用于在你的私有本地网络内,或在 Linux 宿主机和虚拟化的 Windows 访客机之间的虚拟主机专用网络中共享文件。不要将本文视为你公司网络的操作指南,因为本文没有实现必要的网络安全考虑。

从 Windows 访问 Linux

本节介绍从 Windows 文件资源管理器访问用户的 Linux 主目录。

1、安装和配置 Samba

进入你的系统安装 Samba:

dnf install samba

Samba 是一个系统守护进程,其配置文件位于 /etc/samba/smb.conf。它的默认配置应该就可以工作。如果不行,下面这个最小化配置应该可以解决问题:

[global]
        workgroup = SAMBA
        server string = %h server (Samba %v)
        invalid users = root
        security = user
[homes]
        comment = Home Directories
        browseable = no
        valid users = %S
        writable = yes

你可以在该项目网站的 smb.conf 部分找到参数的详细说明。

2、修改 LinuxSE

如果你的 Linux 发行版受 SELinux 保护(比如 Fedora),必须通过以下命令才能通过 Samba 共享主目录:

setsebool -P samba_enable_home_dirs on

通过以下命令查看这个值:

getsebool samba_enable_home_dirs

输出如下:

 title=

3、启用你的用户

Samba 使用一组用户/密码来管理连接权限。通过键入以下命令将你的 Linux 用户添加到该集合中:

smbpasswd -a <你的用户名>

系统提示你输入密码。这是一个 全新 的密码;而不是你账户的当前密码。请输入你想用来登录 Samba 的密码。

键入以下命令得到有 Samba 使用权限的用户列表:

pdbedit -L -v

键入以下命令删除一个用户:

smbpasswd -x <用户名>

4、开启 Samba

既然 Samba 是一个系统守护进程,你可以在 Fedora 上键入以下命令启动它:

systemctl start smb

这将为当前会话开启 Samba 服务。如果想让它自启动,键入以下命令:

systemctl enable smb

在某些系统上,Samba 守护进程注册为 smbd

4、配置防火墙

你的防火墙会默认阻拦 Samba。通过配置防火墙允许 Samba 能永久访问网络。

你可以在命令行执行如下操作:

firewall-cmd --add-service=samba --permanent

或者,你可以使用 firewall-config 工具以图形化方式进行操作:

 title=

5、从 Windows 访问 Samba

在 Windows 中,打开文件资源管理器。在地址栏中,键入两个反斜杠(\\),紧跟你的 Linux 机器的地址(IP 地址或主机名):

 title=

系统将提示你输入登录信息。输入第 3 步中的用户名和密码组合。你现在应该可以访问 Linux 机器上的主目录:

 title=

从 Linux 访问 Windows

以下步骤说明了如何从 Linux 访问共享的 Windows 文件夹。要实现这一点,需要你的 Windows 用户帐户具有管理员权限。

1、启用文件共享

通过点击 “Windows 按钮” > “ 设置 Settings ” > “ 网络和 Internet Network & Internet ” ,或者右键单击任务栏右下角的小监视器图标, 打开网络和共享中心 Open Network an d Sharing Center

 title=

在打开的窗口中,找到你要使用的连接并记下其配置文件。我使用了 以太网 3,它被标记为 公用网络 Public Network

注意:如果你的 PC 经常连接公用网络,请考虑将本地计算机的连接配置文件更改为 私有

记住你的网络配置,然后单击 更改高级共享设置 Change advanced sharing settings

 title=

选择与你的连接对应的配置文件并打开 网络发现 network discovery 文件和打印机共享 file and printer sharing

 title=

2、定义一个共享文件夹

通过右键单击你要共享的文件夹打开上下文菜单,导航到 授予访问权限 Give access to ,然后选择 特定用户... Specific people...

 title=

检查你当前的用户名是否在列表中。点击 共享 Share 将此文件夹标记为共享:

 title=

你可以通过在文件资源管理器的地址栏中输入 \\localhost 来显示所有共享文件夹的列表:

 title=

 title=

3、在 Linux 下挂载共享文件夹

回到你的 Linux 系统,打开一个命令行,然后创建一个新文件夹,用于挂载 Windows 共享:

mkdir ~/WindowsShare

挂载 Windows 共享是使用 mount.cifs 完成的,它应该被默认安装。使用如下命令临时挂载你的共享文件夹:

sudo mount.cifs //<address-of-windows-pc>/MySharedFolder ~/WindowsShare/ -o user=<Windows-user>,uid=$UID

在这个命令里:

  • <address-of-windows-pc> 是 Windows PC 的地址信息(IP 或主机名)
  • <Windows-user> 是允许访问共享文件夹的用户(见步骤 2)

系统将提示你输入 Windows 密码。之后,你将能够使用普通 Linux 用户访问 Windows 上的共享文件夹。

要卸载共享文件夹:

sudo umount ~/WindowsShare/

你还可以在系统启动时挂载 Windows 共享文件夹。按照 这些步骤 相应地配置你的系统。

总结

在这里展示了如何建立临时的文件夹共享访问权限,每次重启后都要重新设置,因此修改成永久访问会更便利。我经常在不同的系统之间来回切换,对我而言设置直接文件访问非常实用。


via: https://opensource.com/article/21/4/share-files-linux-windows

作者:Stephan Avenwedde 选题:lujun9972 译者:hanszhao80 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

提供一个适当的 CMake 配置文件来使其他人可以更容易地构建、使用和贡献你的项目。

 title=

这篇文章是使用开源 DevOps 工具进行 C/C++ 开发系列文章的一部分。如果你从一开始就把你的项目建立在一个功能强大的工具链上,你的开发会更快和更安全。除此之外,这会使别人更容易地参与你的项目。在这篇文章中,我将搭建一个基于 CMakeVSCodium 的 C/C++ 构建系统。像往常一样,相关的示例代码可以在 GitHub 上找到。

我已经测试了在本文中描述的步骤。这是一种适用于所有平台的解决方案。

为什么用 CMake ?

CMake 是一个构建系统生成器,可以为你的项目创建 Makefile。乍一看简单的东西可能相当地复杂。在较高的层次上,你可以定义你的项目的各个部分(可执行文件、库)、编译选项(C/C++ 标准、优化、架构)、依赖关系项(头文件、库),和文件级的项目结构。CMake 使用的这些信息可以在文件 CMakeLists.txt 中获取,它使用一种特殊的描述性语言编写。当 CMake 处理这个文件时,它将自动地侦测在你的系统上已安装的编译器,并创建一个用于启动它的 Makefile 文件。

此外,在 CMakeLists.txt 中描述的配置,能够被很多编辑器读取,像 QtCreator、VSCodium/VSCode 或 Visual Studio 。

示例程序

我们的示例程序是一个简单的命令行工具:它接受一个整数来作为参数,输出一个从 1 到所提供输入值的范围内的随机排列的数字。

$ ./Producer 10
3 8 2 7 9 1 5 10 6 4 

在我们的可执行文件中的 main() 函数,我们只处理输入的参数,如果没有提供一个值(或者一个不能被处理的值)的话,就退出程序。

int main(int argc, char** argv){

    if (argc != 2) {
        std::cerr << "Enter the number of elements as argument" << std::endl;
        return -1;
    }

    int range = 0;
    
    try{
        range = std::stoi(argv[1]);
    }catch (const std::invalid_argument&){
        std::cerr << "Error: Cannot parse \"" << argv[1] << "\" ";
        return -1;
    }

    catch (const std::out_of_range&) {
        std::cerr << "Error: " << argv[1] << " is out of range";
        return -1;
    }

    if (range <= 0) {
        std::cerr << "Error: Zero or negative number provided: " << argv[1];
        return -1;
    }

    std::stringstream data;
    std::cout << Generator::generate(data, range).rdbuf();
}

producer.cpp

实际的工作是在 生成器 中完成的,它将被编译,并将作为一个静态库来链接到我们的Producer 可执行文件。

std::stringstream &Generator::generate(std::stringstream &stream, const int range) {
    std::vector<int> data(range);
    std::iota(data.begin(), data.end(), 1);

    std::random_device rd;
    std::mt19937 g(rd());

    std::shuffle(data.begin(), data.end(), g);

    for (const auto n : data) {

        stream << std::to_string(n) << " ";
    }

    return stream;
}

Generator.cpp

函数 generate 引用一个 std::stringstream 和一个整数来作为一个参数。根据整数 range 的值 n,制作一个在 1n 的范围之中的整数向量,并随后打乱。接下来打乱的向量值转换成一个字符串,并推送到 stringstream 之中。该函数返回与作为参数传递相同的 stringstream 引用。

顶层的 CMakeLists.txt

顶层的 CMakeLists.txt 的是我们项目的入口点。在子目录中可能有多个 CMakeLists.txt 文件(例如,与项目所相关联的库或其它可执行文件)。我们先一步一步地浏览顶层的 CMakeLists.txt

第一行告诉我们处理文件所需要的 CMake 的版本、项目名称及其版本,以及预定的 C++ 标准。

cmake_minimum_required(VERSION 3.14)

project(CPP_Testing_Sample VERSION 1.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

我们用下面一行告诉 CMake 去查看子目录 Generator。这个子目录包括构建 Generator 库的所有信息,并包含它自身的一个 CMakeLists.txt 。我们很快就会谈到这个问题。

add_subdirectory(Generator)

现在,我们将涉及一个绝对特别的功能: CMake 模块 。加载模块可以扩展 CMake 功能。在我们的项目中,我们加载了 FetchContent 模块,这能使我们能够在 CMake 运行时下载外部的资源,在我们的示例中是 GoogleTest

include(FetchContent)

FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/bb9216085fbbf193408653ced9e73c61e7766e80.zip
)
FetchContent_MakeAvailable(googletest)

在接下来的部分中,我们会做一些我们通常在普通的 Makefile 中会做的事: 指定要构建的二进制文件、它们相关的源文件、应该链接的库,以及编译器可以找到头文件的目录。

add_executable(Producer Producer.cpp)

target_link_libraries(Producer PUBLIC Generator)

target_include_directories(Producer PUBLIC "${PROJECT_BINARY_DIR}")

通过下面的语句,我们使 CMake 来在构建文件夹中创建一个名称为 compile_commands.json 的文件。这个文件会展示项目的每个文件的编译器选项。在 VSCodium 中加载该文件,会告知 IntelliSense 功能在哪里查找头文件(查看 文档)。

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

最后的部分为我们的项目定义一些测试。项目使用先前加载的 GoogleTest 框架。单元测试的整个话题将会划归到另外一篇文章。

enable_testing()

add_executable(unit_test unit_test.cpp)

target_link_libraries(unit_test gtest_main)

include(GoogleTest)

gtest_discover_tests(unit_test)

库层次的 CMakeLists.txt

现在,我们来看看包含同名库的子目录 Generator 中的 CMakeLists.txt 文件。这个 CMakeLists.txt 文件的内容更简短一些,除了单元测试相关的命令外,它仅包含 2 条语句。

add_library(Generator STATIC Generator.cpp Generator.h)
target_include_directories(Generator INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

我们使用 add_library(...) 来定义一个新的构建目标:静态的 Generator 库。我们使用语句 target_include_directories(...) 来把当前子目录添加到其它构建目标的头文件的搜索路径之中。我们也具体指定这个属性的范围为类型 INTERFACE:这意味着该属性仅影响链接到这个库的构建目标,而不是库本身。

开始使用 VSCodium

通过使用 CMakeLists.txt 文件中的信息,像 VSCodium 一样的 IDE 可以相应地配置构建系统。如果你还没有使用 VSCodium 或 VS Code 的经验,这个示例项目会是一个很好的起点。首先,转到它们的 网站 ,然后针对你的系统下载最新的安装软件包。打开 VSCodium 并导航到 “ 扩展 Extensions ” 标签页。

为了正确地构建、调试和测试项目,搜索下面的扩展并安装它们:

 title=

如果尚未完成,通过单击起始页的 “ 克隆 Git 存储库 Clone Git Repository ” 来克隆存储库。

 title=

或者手动输入:

git clone https://github.com/hANSIc99/cpp_testing_sample.git

之后,通过输入如下内容来签出标签 devops_1

git checkout tags/devops_1

或者,通过单击 “main” 分支按钮(红色框),并从下拉菜单(黄色框)中选择标签。

 title=

在你打开 VSCodium 内部中的存储库的根文件夹后,CMake Tools 扩展会侦测 CMakeLists.txt 文件并立即扫描你的系统寻找合适的编译器。你现在可以单击屏幕的底部的 “ 构建 Build ” 按钮(红色框)来开始构建过程。你也可以通过单击底部区域的按钮(黄色框)标记来更改编译器,它显示当前活动的编译器。

 title=

要开始调试 Producer 可执行文件,单击调试器符号(黄色框)并从下拉菜单中选择 “ 调试 Debug Producer”(绿色框)。

 title=

如上所述,Producer 可执行文件要求将元素的数量作为一个命令行的参数。命令行参数可以在 .vscode/launch.json 中具体指定。

 title=

好了,你现在能够构建和调试项目了。

结束语

归功于 CMake ,不管你正在运行哪种操作系统,上述步骤应该都能工作。特别是使用与 CMake 相关的扩展,VSCodium 变成了一个强大的 IDE 。我没有提及 VSCodium 的 Git 集成,是因为你已经能够在网络上查找很多的资源。我希望你可以看到:提供一个适当的 CMake 配置文件可以使其他人更容易地构建、使用和贡献于你的项目。在未来的文章中,我将介绍单元测试和 CMake 的测试实用程序 ctest


via: https://opensource.com/article/22/1/devops-cmake

作者:Stephan Avenwedde 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

下次当你为控制台输出的格式而苦恼时,请参考这篇文章及其速查表。

我写文章主要是为了给自己写文档。我在编程时非常健忘,所以我经常会写下有用的代码片段、特殊的特性,以及我使用的编程语言中的常见错误。这篇文章完全切合我最初的想法,因为它涵盖了从 C++ 控制台格式化打印时的常见用例。

像往常一样,这篇文章带有大量的例子。除非另有说明,代码片段中显示的所有类型和类都是 std 命名空间的一部分。所以当你阅读这段代码时,你必须在类型和类的前面加上using namespace std;。当然,该示例代码也可以在 GitHub 上找到。

面向对象的流

如果你曾经用过 C++ 编程,你肯定使用过 cout。当你包含 <iostream> 时,ostream 类型的 cout 对象就进入了作用域。这篇文章的重点是 cout,它可以让你打印到控制台,但这里描述的一般格式化对所有 ostream 类型的流对象都有效。ostream 对象是 basic_ostream 的一个实例,其模板参数为 char 类型。头文件 <iosfwd><iostream> 的包含层次结构的一部分,包含了常见类型的前向声明。

basic_ostream 继承于 basic_ios,该类型又继承于 ios_base。在 cppreference.com 上你可以找到一个显示不同类之间关系的类图。

ios_base 类是所有 I/O 流类的基类。basic_ios 类是一个模板类,它对常见的字符类型进行了 模板特化 specialization ,称为 ios。因此,当你在标准 I/O 的上下文中读到 ios 时,它是 basic_ioschar 类型的模板特化。

格式化流

一般来说,基于 ostream 的流有三种格式化的方法。

  1. 使用 ios_base 提供的格式标志。
  2. 在头文件 <iomanip><ios> 中定义的流修改函数。
  3. 通过调用 << 操作符的 特定重载

所有这些方法都有其优点和缺点,通常取决于使用哪种方法的情况。下面显示的例子混合使用所有这些方法。

右对齐

默认情况下,cout 占用的空间与要打印的数据所需的空间一样大。为了使这种右对齐的输出生效,你必须定义一个行允许占用的最大宽度。我使用格式标志来达到这个目的。

右对齐输出的标志和宽度调整只适用于其后的行。

cout.setf(ios::right, ios::adjustfield);
cout.width(50);
cout << "This text is right justified" << endl;
cout << "This text is left justified again" << endl;

在上面的代码中,我使用 setf 配置了右对齐的输出。我建议你将位掩码 ios::adjustfield 应用于 setf,这将使位掩码指定的所有标志在实际的 ios::right 标志被设置之前被重置,以防止发生组合碰撞。

填充空白

当使用右对齐输出时,默认情况下,空的地方会用空白字符填充。你可以通过使用 setfill 指定填充字符来改变它:

cout << right << setfill('.') << setw(30) << 500 << " pcs" << endl;
cout << right << setfill('.') << setw(30) << 3000 << " pcs" << endl;
cout << right << setfill('.') << setw(30) << 24500 << " pcs" << endl;

代码输出如下:

...........................500 pcs
..........................3000 pcs
.........................24500 pcs

组合

想象一下,你的 C++ 程序记录了你的储藏室库存。不时地,你想打印一份当前库存的清单。要做到这一点,你可以使用以下格式。

下面的代码是左对齐和右对齐输出的组合,使用点作为填充字符,可以得到一个漂亮的列表:

cout << left << setfill('.') << setw(20) << "Flour" << right << setfill('.') << setw(20) << 0.7 << " kg" << endl;
cout << left << setfill('.') << setw(20) << "Honey" << right << setfill('.') << setw(20) << 2 << " Glasses" << endl;
cout << left << setfill('.') << setw(20) << "Noodles" << right << setfill('.') << setw(20) << 800 << " g" << endl;
cout << left << setfill('.') << setw(20) << "Beer" << right << setfill('.') << setw(20) << 20 << " Bottles" << endl;

输出:

Flour...............................0.70 kg
Honey..................................2 Glasses
Noodles..............................800 g
Beer..................................20 Bottles

打印数值

当然,基于流的输出也能让你输出各种变量类型。

布尔型

boolalpha 开关可以让你把布尔型的二进制解释转换为字符串:

cout << "Boolean output without using boolalpha: " << true << " / " << false << endl;
cout << "Boolean output using boolalpha: " << boolalpha << true << " / " << false << endl;

以上几行产生的输出结果如下:

Boolean output without using boolalpha: 1 / 0
Boolean output using boolalpha: true / false

地址

如果一个整数的值应该被看作是一个地址,那么只需要把它投到 void* 就可以了,以便调用正确的重载。下面是一个例子:

unsigned long someAddress = 0x0000ABCD;
cout << "Treat as unsigned long: " << someAddress << endl;
cout << "Treat as address: " << (void*)someAddress << endl;

该代码产生了以下输出:

Treat as unsigned long: 43981
Treat as address: 0000ABCD

该代码打印出了具有正确长度的地址。一个 32 位的可执行文件产生了上述输出。

整数

打印整数是很简单的。为了演示,我使用 setfsetiosflags 来指定数字的基数。应用流修改器 hex/oct 也有同样的效果。

int myInt = 123;

cout << "Decimal: " << myInt << endl;

cout.setf(ios::hex, ios::basefield);
cout << "Hexadecimal: " << myInt << endl;

cout << "Octal: " << resetiosflags(ios::basefield) <<  setiosflags(ios::oct) << myInt << endl;

注意: 默认情况下,没有指示所使用的基数,但你可以使用 showbase 添加一个。

Decimal: 123
Hexadecimal: 7b
Octal: 173

用零填充

0000003
0000035
0000357
0003579

你可以通过指定宽度和填充字符得到类似上述的输出:

cout << setfill('0') << setw(7) << 3 << endl;
cout << setfill('0') << setw(7) << 35 << endl;
cout << setfill('0') << setw(7) << 357 << endl;
cout << setfill('0') << setw(7) << 3579 << endl;

浮点值

如果我想打印浮点数值,我可以选择“固定”和“科学”格式。此外,我还可以指定精度:

double myFloat = 1234.123456789012345;
int defaultPrecision = cout.precision(); // == 2

cout << "Default precision: " << myFloat << endl;
cout.precision(4);
cout << "Modified precision: " << myFloat << endl;
cout.setf(ios::scientific, ios::floatfield);
cout << "Modified precision & scientific format: " << myFloat << endl;
/* back to default */
cout.precision(defaultPrecision);
cout.setf(ios::fixed, ios::floatfield);
cout << "Default precision & fixed format:  " << myFloat << endl;

上面的代码产生以下输出:

Default precision: 1234.12
Modified precision: 1234.1235
Modified precision & scientific format: 1.2341e+03
Default precision & fixed format:  1234.12

时间和金钱

通过 put_money,你可以用正确的、与当地有关的格式来打印货币单位。这需要你的控制台能够输出 UTF-8 字符集。请注意,变量 specialOffering 以美分为单位存储货币价值。

long double specialOffering = 9995;

cout.imbue(locale("en_US.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
cout.imbue(locale("de_DE.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
cout.imbue(locale("ru_RU.UTF-8"));
cout  << showbase << put_money(specialOffering) << endl;

iosimbue 方法让你指定一个地区。通过命令 locale -a,你可以得到你系统中所有可用的地区标识符的列表。

$99.95
99,950€
99,950₽

(不知道出于什么原因,在我的系统上,它打印的欧元和卢布有三个小数位,对我来说看起来很奇怪,但这也许是官方的格式。)

同样的原则也适用于时间输出。函数 put_time 可以让你以相应的地区格式打印时间。此外,你可以指定时间对象的哪些部分被打印出来。

time_t now = time(nullptr);
tm localtm = *localtime(&now);


cout.imbue(locale("en_US.UTF-8"));
cout << "en_US : " << put_time(&localtm, "%c") << endl;
cout.imbue(locale("de_DE.UTF-8"));
cout << "de_DE : " << put_time(&localtm, "%c") << endl;
cout.imbue(locale("ru_RU.UTF-8"));
cout << "ru_RU : " << put_time(&localtm, "%c") << endl;

格式指定符 %c 会打印一个标准的日期和时间字符串:

en_US : Tue 02 Nov 2021 07:36:36 AM CET
de_DE : Di 02 Nov 2021 07:36:36 CET
ru_RU : Вт 02 ноя 2021 07:36:36

创建自定义的流修改器

你也可以创建你自己的流。下面的代码在应用于 ostream 对象时插入了一个预定义的字符串:

ostream& myManipulator(ostream& os) {
    string myStr = ">>>Here I am<<<";
    os << myStr;
    return os;
}

另一个例子: 如果你有重要的事情要说,就像互联网上的大多数人一样,你可以使用下面的代码在你的信息后面根据重要程度插入感叹号。重要程度被作为一个参数传递:

struct T_Importance {
     int levelOfSignificance;
};

T_Importance importance(int lvl){
    T_Importance x = {.levelOfSignificance = lvl };
    return x;
}

ostream& operator<<(ostream& __os, T_Importance t){

    for(int i = 0; i < t.levelOfSignificance; ++i){
        __os.put('!');
    }
    return __os;
}

这两个修饰符现在都可以简单地传递给 cout

cout << "My custom manipulator: " << myManipulator << endl;

cout << "I have something important to say" << importance(5) << endl;

产生以下输出:

My custom manipulator: >>>Here I am<<<

I have something important to say!!!!!

结语

下次你再纠结于控制台输出格式时,我希望你记得这篇文章及其 速查表

在 C++ 应用程序中,coutprintf 的新邻居。虽然使用 printf 仍然有效,但我可能总是喜欢使用 cout。特别是与定义在 <ios> 中的修改函数相结合,会产生漂亮的、可读的代码。


via: https://opensource.com/article/21/11/c-stdcout-cheat-sheet

作者:Stephan Avenwedde 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

一旦你理解了一般原则,C++ 类成员函数指针不再那么令人生畏。

 title=

如果你正在寻找性能、复杂性或许多可能的解决方法来解决问题,那么在涉及到极端的情况下,C++ 总是一个很好的选择。当然,功能通常伴随着复杂性,但是一些 C++ 的特性几乎难以分辨。根据我的观点,C++ 的 类成员函数指针 也许是我接触过的最复杂的表达式,但是我会先从一些较简单的开始。

文章中的例子可以在我的 Github 仓库 里找到。

C 语言:函数指针

让我们先从一些基础开始:假设你有一个函数接收两个整数作为参数返回一个整数:

int sum(int a, int b) {
    return a+b;
}

在纯 C 语言中,你可以创建一个指向这个函数的指针,将其分配给你的 sum(...) 函数,通过解引用来调用它。函数的签名(参数、返回类型)必须符合指针的签名。除此之外,一个函数指针表现和普通的指针相同:

int (*funcPtrOne)(int, int);

funcPtrOne = &sum;

int resultOne = funcPtrOne(2, 5);

如果你使用指针作为参数并返回一个指针,这会显得很丑陋:

int *next(int *arrayOfInt){
    return ++arrayOfInt;
}

int *(*funcPtrTwo)(int *intPtr);

funcPtrTwo = &next;

int resultTwo = *funcPtrTwo(&array[0]);

C 语言中的函数指针存储着子程序的地址。

指向类成员函数的指针

让我们来进入 C++:好消息是你也许不需要使用类成员函数指针,除非在一个特别罕见的情况下,比如说接下来的例子。首先,你已经知道定义一个类和其中一个成员函数:

class MyClass
{
public:

    int sum(int a, int b) {
        return a+b;
    }

};

1、定义一个指针指向某一个类中一个成员函数

声明一个指针指向 MyClass 类成员函数。在此时,你并不知道想调用的具体函数。你仅仅声明了一个指向 MyClass 类中任意成员函数的指针。当然,签名(参数、返回值类型)需要匹配你接下想要调用的 sum(...) 函数:

int (MyClass::*methodPtrOne)(int, int);

2、赋值给一个具体的函数

为了和 C 语言(或者 静态成员函数#Static_method))对比,类成员函数指针不需要指向绝对地址。在 C++ 中,每一个类中都有一个虚拟函数表(vtable)用来储存每个成员函数的地址偏移量。一个类成员函数指针指向 vtable 中的某个条目,因此它也只存储偏移值。这样的原则使得 多态 变得可行。

因为 sum(...) 函数的签名和你的指针声明匹配,你可以赋值签名给它:

methodPtrOne = &MyClass::sum;

3、调用成员函数

如果你想使用指针调用一个类成员函,你必须提供一个类的实例:

MyClass clsInstance;
int result = (clsInstance.*methodPtrOne)(2,3);

你可以使用 . 操作符来访问,使用 * 对指针解引用,通过提供两个整数作为调用函数时的参数。这是丑陋的,对吧?但是你可以进一步应用。

在类内使用类成员函数指针

假设你正在创建一个带有后端和前端的 客户端/服务器 原理架构的应用程序。你现在并不需要关心后端,相反的,你将基于 C++ 类的前端。前端依赖于后端提供的数据完成初始化,所以你需要一个额外的初始化机制。同时,你希望通用地实现此机制,以便将来可以使用其他初始化函数(可能是动态的)来拓展你的前端。

首先定义一个数据类型用来存储初始化函数(init)的指针,同时描述何时应调用此函数的信息(ticks):

template<typename T>
struct DynamicInitCommand {
    void (T::*init)();     // 指向额外的初始化函数
    unsigned int ticks;    // 在 init() 调用后 ticks 的数量
};

下面一个 Frontend 类示例代码:

class  Frontend
{
public:

    Frontend(){
        DynamicInitCommand<Frontend> init1, init2, init3;

        init1 = { &Frontend::dynamicInit1, 5};
        init2 = { &Frontend::dynamicInit2, 10};
        init3 = { &Frontend::dynamicInit3, 15};

        m_dynamicInit.push_back(init1);
        m_dynamicInit.push_back(init2);
        m_dynamicInit.push_back(init3);
    }
   
    void  tick(){
        std::cout << "tick: " << ++m_ticks << std::endl;
       
        /* 检查延迟初始化 */
        std::vector<DynamicInitCommand<Frontend>>::iterator  it = m_dynamicInit.begin();

        while (it != m_dynamicInit.end()){
            if (it->ticks < m_ticks){
                 
                if(it->init)
                    ((*this).*(it->init))(); // 这里是具体调用

                it = m_dynamicInit.erase(it);

            } else {
                it++;
            }
        }
    }
   
    unsigned  int  m_ticks{0};
   
private:

    void  dynamicInit1(){
        std::cout << "dynamicInit1 called" << std::endl;
    };

    void  dynamicInit2(){
        std::cout << "dynamicInit2 called" << std::endl;
    }

    void  dynamicInit3(){
        std::cout << "dynamicInit3 called" << std::endl;
    }

    unsigned  int  m_initCnt{0};
    std::vector<DynamicInitCommand<Frontend> > m_dynamicInit;
};

Frontend 完成实例化后,tick() 函数会被后端以固定的时间时间调用。例如,你可以每 200 毫秒调用一次:

int  main(int  argc, char*  argv[]){
    Frontend frontendInstance;

    while(true){
        frontendInstance.tick(); // 仅用于模拟目的
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

Fronted 有三个额外的初始化函数,它们必须根据 m_ticks 的值来选择调用哪个。在 ticks 等于何值调用哪个初始化函数的信息存储在数组 m_dynamicInit 中。在构造函数(Frontend())中,将此信息附加到数组中,以便在 5、10 和 15 个 tick 后调用其他初始化函数。当后端调用 tick() 函数时,m_ticks 值会递增,同时遍历数组 m_dynamicInit 以检查是否必须调用初始化函数。

如果是这种情况,则必须通过引用 this 指针来取消引用成员函数指针:

((*this).*(it->init))()

总结

如果你并不熟悉类成员函数指针,它们可能会显得有些复杂。我做了很多尝试和经历了很多错误,花了一些时间来找到正确的语法。然而,一旦你理解了一般原理后,方法指针就变得不那么可怕了。

这是迄今为止我在 C++ 中发现的最复杂的语法。 你还知道更糟糕的吗? 在评论中发布你的观点!


via: https://opensource.com/article/21/2/ccc-method-pointers

作者:Stephan Avenwedde 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解 Linux 如何使用库,包括静态库和动态库的差别,有助于你解决依赖问题。

 title=

Linux 从某种意义上来说就是一堆相互依赖的静态和动态库。对于 Linux 系统新手来说,库的整个处理过程简直是个迷。但对有经验的人来说,被构建进操作系统的大量共享代码对于编写新应用来说却是个优点。

为了让你熟悉这个话题,我准备了一个小巧的 应用例子 来展示在普通的 Linux 发行版(在其他操作系统上未验证)上是经常是如何处理库的。为了用这个例子来跟上这个需要动手的教程,请打开命令行输入:

$ git clone https://github.com/hANSIc99/library_sample
$ cd library_sample/
$ make
cc -c main.c -Wall -Werror
cc -c libmy_static_a.c -o libmy_static_a.o -Wall -Werror
cc -c libmy_static_b.c -o libmy_static_b.o -Wall -Werror
ar -rsv libmy_static.a libmy_static_a.o libmy_static_b.o
ar: creating libmy_static.a
a - libmy_static_a.o
a - libmy_static_b.o
cc -c -fPIC libmy_shared.c -o libmy_shared.o
cc -shared -o libmy_shared.so libmy_shared.o
$ make clean
rm *.o

当执行完这些命令,这些文件应当被添加进目录下(执行 ls 来查看):

my_app
libmy_static.a
libmy_shared.so

关于静态链接

当你的应用链接了一个静态库,这个库的代码就变成了可执行文件的一部分。这个动作只在链接过程中执行一次,这些静态库通常以 .a 扩展符结尾。

静态库是多个 目标 object 文件的 归档 archive ar)。这些目标文件通常是 ELF 格式的。ELF 是 可执行可链接格式 Executable and Linkable Format 的简写,它与多个操作系统兼容。

file 命令的输出可以告诉你静态库 libmy_static.aar 格式的归档文件类型。

$ file libmy_static.a
libmy_static.a: current ar archive

使用 ar -t,你可以看到归档文件的内部。它展示了两个目标文件:

$ ar -t libmy_static.a
libmy_static_a.o
libmy_static_b.o

你可以用 ax -x <archive-file> 命令来提取归档文件的文件。被提出的都是 ELF 格式的目标文件:

$ ar -x libmy_static.a
$ file libmy_static_a.o
libmy_static_a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

关于动态链接

动态链接指的是使用共享库。共享库通常以 .so 的扩展名结尾(“ 共享对象 shared object ” 的简写)。

共享库是 Linux 系统中依赖管理的最常用方法。这些共享库在应用启动前被载入内存,当多个应用都需要同一个库时,这个库在系统中只会被加载一次。这个特性减少了应用的内存占用。

另外一个值得注意的地方是,当一个共享库的 bug 被修复后,所有引用了这个库的应用都会受益。但这也意味着,如果一个 bug 还没被发现,那所有相关的应用都会遭受这个 bug 影响(如果这个应用使用了受影响的部分)。

当一个应用需要某个特定版本的库,但是 链接器 linker 只知道某个不兼容版本的位置,对于初学者来说这个问题非常棘手。在这个场景下,你必须帮助链接器找到正确版本的路径。

尽管这不是一个每天都会遇到的问题,但是理解动态链接的原理总是有助于你修复类似的问题。

幸运的是,动态链接的机制其实非常简洁明了。

为了检查一个应用在启动时需要哪些库,你可以使用 ldd 命令,它会打印出给定文件所需的动态库:

$ ldd my_app
        linux-vdso.so.1 (0x00007ffd1299c000)
        libmy_shared.so => not found
        libc.so.6 => /lib64/libc.so.6 (0x00007f56b869b000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f56b8881000)

可以注意到 libmy_shared.so 库是代码仓库的一部分,但是没有被找到。这是因为负责在应用启动之前将所有依赖加载进内存的动态链接器没有在它搜索的标准路径下找到这个库。

对新手来说,与常用库(例如 bizp2)版本不兼容相关的问题往往十分令人困惑。一种方法是把该仓库的路径加入到环境变量 LD_LIBRARY_PATH 中来告诉链接器去哪里找到正确的版本。在本例中,正确的版本就在这个目录下,所以你可以导出它至环境变量:

$ LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH

现在动态链接器知道去哪找库了,应用也可以执行了。你可以再次执行 ldd 去调用动态链接器,它会检查应用的依赖然后加载进内存。内存地址会在对象路径后展示:

$ ldd my_app
        linux-vdso.so.1 (0x00007ffd385f7000)
        libmy_shared.so => /home/stephan/library_sample/libmy_shared.so (0x00007f3fad401000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f3fad21d000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f3fad408000)

想知道哪个链接器被调用了,你可以用 file 命令:

$ file my_app
my_app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=26c677b771122b4c99f0fd9ee001e6c743550fa6, for GNU/Linux 3.2.0, not stripped

链接器 /lib64/ld-linux-x86–64.so.2 是一个指向 ld-2.30.so 的软链接,它也是我的 Linux 发行版的默认链接器:

$ file /lib64/ld-linux-x86-64.so.2
/lib64/ld-linux-x86-64.so.2: symbolic link to ld-2.31.so

回头看看 ldd 命令的输出,你还可以看到(在 libmy_shared.so 边上)每个依赖都以一个数字结尾(例如 /lib64/libc.so.6)。共享对象的常见命名格式为:

libXYZ.so.<MAJOR>.<MINOR>

在我的系统中,libc.so.6 也是指向同一目录下的共享对象 libc-2.31.so 的软链接。

$ file /lib64/libc.so.6
/lib64/libc.so.6: symbolic link to libc-2.31.so

如果你正在面对一个应用因为加载库的版本不对导致无法启动的问题,有很大可能你可以通过检查整理这些软链接或者确定正确的搜索路径(查看下方“动态加载器:ld.so”一节)来解决这个问题。

更为详细的信息请查看 ldd 手册页

动态加载

动态加载的意思是一个库(例如一个 .so 文件)在程序的运行时被加载。这是使用某种特定的编程方法实现的。

当一个应用使用可以在运行时改变的插件时,就会使用动态加载。

查看 dlopen 手册页 获取更多信息。

动态加载器:ld.so

在 Linux 系统中,你几乎总是正在跟共享库打交道,所以必须有个机制来检测一个应用的依赖并将其加载进内存中。

ld.so 按以下顺序在这些地方寻找共享对象:

  1. 应用的绝对路径或相对路径下(用 GCC 编译器的 -rpath 选项硬编码的)
  2. 环境变量 LD_LIBRARY_PATH
  3. /etc/ld.so.cache 文件

需要记住的是,将一个库加到系统库归档 /usr/lib64 中需要管理员权限。你可以手动拷贝 libmy_shared.so 至库归档中来让应用可以运行,而避免设置 LD_LIBRARY_PATH

unset LD_LIBRARY_PATH
sudo cp libmy_shared.so /usr/lib64/

当你运行 ldd 时,你现在可以看到归档库的路径被展示出来:

$ ldd my_app
        linux-vdso.so.1 (0x00007ffe82fab000)
        libmy_shared.so => /lib64/libmy_shared.so (0x00007f0a963e0000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f0a96216000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0a96401000)

在编译时定制共享库

如果你想你的应用使用你的共享库,你可以在编译时指定一个绝对或相对路径。

编辑 makefile(第 10 行)然后通过 make -B 来重新编译程序。然后 ldd 输出显示 libmy_shared.so 和它的绝对路径一起被列出来了。

把这个:

CFLAGS =-Wall -Werror -Wl,-rpath,$(shell pwd)

改成这个(记得修改用户名):

CFLAGS =/home/stephan/library_sample/libmy_shared.so

然后重新编译:

$ make

确认下它正在使用你设定的绝对路径,你可以在输出的第二行看到:

$ ldd my_app
    linux-vdso.so.1 (0x00007ffe143ed000)
        libmy_shared.so => /lib64/libmy_shared.so (0x00007fe50926d000)
        /home/stephan/library_sample/libmy_shared.so (0x00007fe509268000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fe50909e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fe50928e000)

这是个不错的例子,但是如果你在编写给其他人用的库,它是怎样工作的呢?新库的路径可以通过写入 /etc/ld.so.conf 或是在 /etc/ld.so.conf.d/ 目录下创建一个包含路径的 <library-name>.conf 文件来注册至系统。之后,你必须执行 ldconfig 命令来覆写 ld.so.cache 文件。这一步有时候在你装了携带特殊的共享库的程序来说是不可省略的。

查看 ld.so 的手册页 获取更多详细信息。

怎样处理多种架构

通常来说,32 位和 64 位版本的应用有不同的库。下面列表展示了不同 Linux 发行版库的标准路径:

红帽家族

  • 32 位:/usr/lib
  • 64 位:/usr/lib64

Debian 家族

  • 32 位:/usr/lib/i386-linux-gnu
  • 64 位:/usr/lib/x86_64-linux-gnu

Arch Linux 家族

  • 32 位:/usr/lib32
  • 64 位:/usr/lib64

FreeBSD(技术上来说不算 Linux 发行版)

  • 32 位:/usr/lib32
  • 64 位:/usr/lib

知道去哪找这些关键库可以让库链接失效的问题成为历史。

虽然刚开始会有点困惑,但是理解 Linux 库的依赖管理是一种对操作系统掌控感的表现。在其他应用程序中运行这些步骤,以熟悉常见的库,然后继续学习怎样解决任何你可能遇到的库的挑战。


via: https://opensource.com/article/20/6/linux-libraries

作者:Stephan Avenwedde 选题:lujun9972 译者:tt67wq 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出