Mr. Kin's Blog

计算机知识分享/软件应用讲解

1   背景

因联通赠送的光猫性能不支持双lan口长时间同时上网,故从海鲜市场(小黄鱼)上淘了一块n3530小工控机(准系统,无内存条),计划用来安装pve虚拟机用以作软路由处理宽带拨号和网关ip分配,光猫仅作为一个光电信号转换器。

2   机器配置

  • CPU:Intel(R) Pentium(R) CPU N3530
  • 内存:海力士DDR3L-8GB单根(从老笔记本电脑上拆一根DDR3条子给它用)
  • 硬盘:海力士SH920 msata 256GB
  • 网卡:双网卡intel I211+Realtek mini-pcie RTL8211E/F(螃蟹卡,后期自己购买加装至mini pcie无线网卡口位置)

3   安装pve

3.1   机器BIOS的优化设置

  • 设置来电自启
  • 开启快速启动

3.2   安装步骤

  1. 下载pve系统镜像:点击跳转(我的情况就是需要下载pve 7.4的iso镜像文件)
  2. 制作U盘启动项:ventoy。拷贝镜像iso文件到U盘,启动选择pve镜像进入,选择图形安装界面。
  3. 同意协议。
  4. 点击option按钮,选择硬盘和ext4格式(我的情况只有一个硬盘,不用设置option,直接下一步)
  5. 国家选择china,时区会自动修改为上海,键盘布局不改。
  6. 设置密码,邮箱随意。
  7. 管理网口,默认第一个做管理(enp1s0,pve系统里面识别的第一个网口,靠近hdmi接口的rj45),第二个做wan口(方便后续维护)。域名hostname设置为pve.lan(这项随便填写都行,有能用的域名可填写上去,可解析到机器用域名管理pve虚拟机);ip:192.168.1.2;掩码:255.255.255.0;网关:192.168.1.1(留给ikuai的地址),DNS:223.5.5.5。
  8. 查看确认已设置的内容(弹窗),确认安装。
  9. 访问管理地址并登录:https://192.168.1.2:8006。
  10. 设置网卡并确保pve能联网。

3.3   升级步骤(7.4->8.0)

  1. 替换软件源和企业软件源(国内镜像)

    /etc/apt/sources.list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #deb http://ftp.debian.org/debian bullseye main contrib
    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free

    #deb http://ftp.debian.org/debian bullseye-updates main contrib
    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free

    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
    # security updates
    #deb http://security.debian.org bullseye-security main contrib
    deb [trusted=yes] https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-security main contrib non-free

    删除pve-enterprise.list,新建pve-no-subscription.list。

    /etc/apt/sources.list.d/pve-no-subscription.list

    1
    2
    3
    cd /etc/apt/sources.list.d
    mv pve-enterprise.list pve-enterprise.list.bak
    echo deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve bullseye pve-no-subscription > pve-no-subscription.list
  2. 升级到7.4最新的版本
    1
    2
    3
    4
    5
    6
    7
    8
    apt update -y && apt dist-upgrade -y

    sed -i_orig "s/data.status === 'Active'/true/g" /usr/share/pve-manager/js/pvemanagerlib.js
    sed -i_orig "s/if (res === null || res === undefined || \!res || res/if(/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
    sed -i_orig "s/.data.status.toLowerCase() !== 'active'/false/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
    systemctl restart pveproxy

    pveversion
  3. 检查兼容性:pve7to8
  4. 替换8.0软件源和企业软件源(国内镜像)

    /etc/apt/sources.list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # deb http://ftp.debian.org/debian bookworm main contrib
    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free

    # deb http://ftp.debian.org/debian bookworm-updates main contrib
    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free

    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free
    # security updates
    # deb http://security.debian.org bookworm-security main contrib
    deb [trusted=yes] https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-security main contrib non-free

    删除pve-enterprise.list,新建pve-no-subscription.list。

    /etc/apt/sources.list.d/pve-no-subscription.list

    1
    2
    3
    cd /etc/apt/sources.list.d
    mv pve-enterprise.list pve-enterprise.list.bak
    echo deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve bookworm pve-no-subscription > pve-no-subscription.list
  5. 开始正式升级
    1
    2
    apt update
    apt dist-upgrade
    升级中会出现一些交互界面,下面是官网的建议(不清楚的选项,可以选择推荐参数)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /etc/issue -> Proxmox VE will auto-generate this file on boot, and it has only cosmetic effects on the login console.
    Using the default "No" (keep your currently-installed version) is safe here.

    /etc/lvm/lvm.conf -> Changes relevant for Proxmox VE will be updated, and a newer config version might be useful.
    If you did not make extra changes yourself and are unsure it's suggested to choose "Yes" (install the package maintainer's version) here.

    /etc/ssh/sshd_config -> If you have not changed this file manually, the only differences should be a replacement of ChallengeResponseAuthentication no with KbdInteractiveAuthentication no and some irrelevant changes in comments (lines starting with #).
    If this is the case, both options are safe, though we would recommend installing the package maintainer's version in order to move away from the deprecated ChallengeResponseAuthentication option. If there are other changes, we suggest to inspect them closely and decide accordingly.

    /etc/default/grub -> Here you may want to take special care, as this is normally only asked for if you changed it manually, e.g., for adding some kernel command line option.
    It's recommended to check the difference for any relevant change, note that changes in comments (lines starting with #) are not relevant.
    If unsure, we suggested to selected "No" (keep your currently-installed version)
  6. 去除未订阅提示
    1
    2
    3
    4
    sed -i_orig "s/data.status === 'Active'/true/g" /usr/share/pve-manager/js/pvemanagerlib.js
    sed -i_orig "s/if (res === null || res === undefined || \!res || res/if(/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
    sed -i_orig "s/.data.status.toLowerCase() !== 'active'/false/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
    systemctl restart pveproxy

升级若遇到提示Upgrade wants to remove package 'proxmox-ve'的话,在升级前尝试执行apt remove linux-image-amd64

3.4   安装遇到的坑

3.4.1   安装到99%时出现unable to install the EFI boot loader on '/dev/sda'

报错信息:

1
2
bootloader setup errors:
- failed to prepare EFI boot using Grub on '/dev/sda2': unable to install the EFI boot loader on '/dev/sda'

原因:pve 8.0版本镜像īso不兼容此台机器,估计是bug。参考其他帖子说的只保留usb的uefi设备,依旧也会报错。并且因为机器的bios设置也比较繁琐,我反复尝试将其设置为csm模式,机器始终无法以csm模式启动,估计该主板的bios也是魔改过有bug的。因此导致也无法以csm模式安装pve。

解决方法:安装pve 7.4版本,再升级到pve 8.x。

3.4.2   USB键盘一直插着时会无法响应的,无法进入并操控BIOS,只有CTRL+ALT+DELETE可以响应

原因:估计是BIOS的驱动BUG。

解决方法:先不插键盘启动机器,看屏幕等待主板自检代码走到99阶段,快速插上键盘,摁DEL键。

3.4.3   hdmi屏幕不显bios

主板BIOS驱动不支持普通HDMI小屏幕,自检阶段和BIOS界面显示白屏或者花屏,接大屏驱动板兼容性好点。

3.4.4   后面加装的RTL8211网卡不识别

原因:因为安装pve系统之前未加装好螃蟹卡RTL8211,因此后续加装之后,pve系统并未识别到。

解决方法:重装pve系统。(驱动源码编译方案,也有编译报错,不好解决,推荐是重装pve,让pve自动打驱动)

3.4.5   pve重启bug,看门狗导致重启很长时间

原因:未查明,只要一重启,就会长时间卡在看门狗的错误提示。

错误提示:

1
2
3
watchgod: watchdog0: watchdog did't did not stop!

systemd-shutdown: Failed to finalzie DM services, ignorening.

解决方法:无法解决,只能长时间等待完成重启或者强制关机重开。

4   硬件直通

IOMMU允许系统设备在虚拟内存中进行寻址,也就是将虚拟内存地址映射为物理内存地址,让实体设备可以在虚拟的内存环境中工作,这样可以帮助系统扩充内存容量,提升性能。

换而言之,IOMMU可以使VM虚拟机能够接入一些物理设备,比如PCIe中的网卡、声卡、显卡,VM虚拟机可以直接或间接使用这些设备。

在创建虚拟机时,芯片组一定要q35。因为Q35,才能PCIE直通,否则就是PCI直通。

4.1   检查硬件是否支持直通(BIOS内查看Advanced高级选项)

  1. 开启CPU虚拟化:Intel: VT-X(Intel Virtual Technology)。AMD: AMD-V(SVM, Secure Virtual Machine)。
  2. 为CPU开启硬件虚拟功能:Intel: Intel VT-d。AMD: IOMMU。
  3. 部分主板会有和Intel VT-d/IOMMU相关联的Memory Remap Feature设置。

如果没有VT-d或者IOMMU功能选项,则主板不支持虚拟机直通硬件。intel要b75以上芯片组才支持,即需要从intel 4代酷睿处理器开始才支持。

4.2   pve提示No IOMMU detected

当pve未启用IOMMU或者硬件不支持直通(已启用IOMMU)时,在ProxmoxVE(PVE)的VM/VPS中添加PCI设备时候提示:No IOMMU detected, please activate it.See Documentation for further information.

4.3   pve启用IOMMU

内核版本为5.15或之前的,在ProxmoxVE(PVE)系统内核中,需要手动启用IOMMU。5.15版本以后的ProxmoxVE(PVE)系统内核自带默认开启了IOMMU支持。

但我的硬件本身不支持VT-d,因此并未自动启用IOMMU,也可能是因为从7.4升级到8.0的缘故。

  1. 编辑grub文件:nano /etc/default/grub
  2. 找到GRUB_CMDLINE_LINUX_DEFAULT这一行:
    1
    2
    3
    4
    5
    6
    7
    ...
    GRUB_DEFAULT=0
    GRUB_TIMEOUT=5
    GRUB_DISTRIBUTOR=`lsb_release -i -s <strong>2</strong>> /dev/null || echo Debian`
    GRUB_CMDLINE_LINUX_DEFAULT="quiet"
    GRUB_CMDLINE_LINUX=""
    ...
  3. 根据CPU类型修改
    1
    2
    3
    ...
    GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on"
    ...
    1
    2
    3
    ...
    GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on"
    ...
  4. 更新grub:update-grub
  5. 重启PVE
  1. 编辑cmdline:nano /etc/kernel/cmdline。(若不存在,则新建该文件)
  2. 根据CPU类型在第一行末尾添加:
    1
    quiet intel_iommu=on
    1
    quiet amd_iommu=on

验证IOMMU是否启用:dmesg | grep -e DMAR -e IOMMU。输出显示DMAR: IOMMU enabled证明已启用,虚拟机分配PCI设备时可见。若无输出,需排查故障。

4.4   pve启用PT模式

PT模式:会在IOMMU需要使用时候才启动,适配器不需要使用 DMA 转换到内存,因此可以提高其他没有分配过设备的性能。

  1. 编辑grub文件:nano /etc/default/grub
  2. 找到GRUB_CMDLINE_LINUX_DEFAULT这一行:
    1
    2
    3
    4
    5
    6
    7
    ...
    GRUB_DEFAULT=0
    GRUB_TIMEOUT=5
    GRUB_DISTRIBUTOR=`lsb_release -i -s <strong>2</strong>> /dev/null || echo Debian`
    GRUB_CMDLINE_LINUX_DEFAULT="quiet"
    GRUB_CMDLINE_LINUX=""
    ...
  3. 附加参数iommu=pt,Intel和AMD芯片均可使用这个参数。
  4. 添加模块设置(5.4内核需自行添加,现行版本自带有添加):nano /etc/modules。没有如下内容需添加。
    1
    2
    3
    4
    vfio
    vfio_iommu_type1
    vfio_pci
    vfio_virqfd
  5. 刷新 initramfs:update-initramfs -u -k all
  6. 更新 grub:update-grub

5   创建PVE虚拟机

若机器硬件性能不太足够的话,不建议安装win系统,win系统极度消耗硬件性能(即便是空载待机状态也消耗很大),而这影响openwrt的性能。比如我的情况:安装了tiny10系统,千兆网线,宽带500M实测不到300Mbp/s(这里是指用广东联通宽带测速平台测试的结果,实际ikuai主路由测试速率却又正常),局域网速率最高只能到700Mbp/s的速率。关闭后tiny10后,实测宽带速率恢复正常,局域网速率跑到900Mbp/s出头)

5.1   PVE基础设置

  1. 访问管理地址并登录:https://192.168.1.2:8006,帐号:root。
  2. 根据硬件网卡情况,添加并设置好虚拟网桥Linux Bridge:创建>Linux Bridge>桥接接口填写物理网口地址名称。以我的机器为例,机器三个物理网口,PVE新建5个网桥,前三个网桥和物理网口绑定,第四个网桥用作iKuai和OpenWrt之间的传输,第五个网桥用作其他虚拟机系统的传输。

5.2   安装iKuai

  1. 下载iKuai镜像:点击跳转
  2. local-内容-上传:iKuai的安装镜像。
  3. 创建虚拟机,命名虚拟机名称ikuai,VM编号100。
  4. CD/DVD光盘镜像文件选择刚上传的ISO镜像。
  5. 硬盘分配2G,添加EFI磁盘(不勾选添加密钥),CPU给2核(KVM64),内存分配4G(64位iKuai要求4G内存),网卡模型选择VirtIO(半虚拟化)。
  6. 完成新建后,在硬件标签页中继续完成添加所有网桥,网桥的防火墙全部关掉(使用iKuai的),模型都选择半虚拟化。
  7. 在选项标签页中把「开机自启动」开启,启动顺序设置为1,引导顺序只开disk‘xxxx’磁盘,把网络启动关掉,CD启动上移到第一位。
  8. 点击启动虚拟机,完成安装即可。
  9. iKuai内识别的第一个网卡设置为lan1。
  10. 输入字母o回车进入其「其他选项」,开启外网访问web。
  11. 访问管理地址并登录:https://192.168.1.1:80。默认用户:admin。默认密码:admin。

5.3   安装OpenWrt

5.3.1   OpenWrt镜像的选择

  • Bleach OpenWrt:推荐。内置SmartDNS,软件包空间设置为1G,剩余五百多兆。
  • eSir GDQ 高大全:eSir的高大全固件。不推荐。没有内置SmartDNS拓展(我测试时安装SmartDNS并未出现有服务项,无效)。并且相较于Blench版本,较为繁琐。软件包空间只剩余几兆,需自己手动扩盘。

5.3.2   安装步骤

  1. 创建虚拟机,命名虚拟机名称openwrt,VM编号101。
  2. CD/DVD光盘镜像文件设置为无介质。
  3. 硬盘随便分配(后续会删除),添加EFI磁盘(不勾选添加密钥),CPU给4核(KVM64),内存分配4G,网卡模型选择VirtIO(半虚拟化)。
  4. 网桥只选择第四个网桥(用作iKuai和OpenWrt之间的传输),网卡的防火墙全部关掉(使用iKuai的),模型选择半虚拟化。
  5. 下载OpenWrt镜像:见上一小节
  6. local-内容-上传:OpenWrt的镜像。
  7. 向虚拟机导入OpenWrt镜像:qm importdisk 101 /var/lib/vz/template/iso/bleach-plus-20230826-openwrt-x86-64-generic-squashfs-combined-efi.img local-lvm。(直接写盘即可,无需安装)。
  8. 在pve管理页面中,加载刚才导入镜像生成的磁盘。
  9. 在选项标签页中把「开机自启动」开启,启动顺序设置为2,引导顺序只开disk‘xxxx’磁盘,把网络启动关掉。
  10. 点击启动虚拟机。
  11. pve中用shell:vi /etc/config/network,将lan口ip改为192.168.1.3reboot重启。vim编辑:按i进入修改模式,按esc退出编辑模式,输入 :wq回车保存修改。
  12. 访问管理地址并登录:https://192.168.1.3。默认用户:root。默认密码:password。

5.4   创建虚拟机遇到的坑

5.4.1   添加EFI磁盘后,启动无法引导磁盘,出现shell界面。

原因:创建EFI磁盘时,勾选了添加密钥。

解决方法:创建EFI磁盘时,取消勾选添加密钥。

5.4.2   安装windows镜像,无法识别找到磁盘

原因:scsi不适合windows镜像,windows安装解决识别不到硬盘。

解决方法:创建硬盘时,总线/设备选择:sata硬盘或者IDE。

5.4.3   安装windows镜像,网卡无驱动

现象:当网卡设置为virtio模型时,安装并进入到windows系统后,网卡无驱动。

原因:windows系统自带的驱动无此虚拟硬件的驱动。

解决方式:加载virtio镜像,并运行x64程序安装驱动即可。

VirtIO镜像下载地址:点击跳转

6   iKuai +OpenWRT 做旁路由网络拓扑

iKuai作为主路由,负责拨号及DHCP,OpenWRT做旁路由。SmartDNS+AdGuardHome设置分流与去广告。SmartDNS作为DNS管理并提供DNS缓存,实现国内国外DNS分流,彻底解决DNS污染问题、实现秒开网页。同时搭配AdGuardHome实现整个局域网去广告。

DNS转发流程:设置最核心的部分就是DNS转发端口的衔接,就是把DNSMASQ、Adguardhome、SmartDNS三个插件里的DNS服务器功能分成三个层级,实现层层转发。依次是第一级DNSMASQ,第二级Adguardhome,第三级SmartDNS,第四级OpenClash。

6.1   iKuai的设置

  • 系统设置>重启关机>添加一个重启计划:每天05:00时重启。
  • 网络设置>内外网设置
    • 外网接口(选择iKuai识别到的第二个网口):填入宽带拨号信息。
    • 内网接口(iKuai识别到的第一个网口,IP地址192.168.1.1):链路桥接(选择剩余的其他全部接口)
  • DHCP设置>DHCP服务端
    • 单iKuai版:
      • 客户端地址:192.168.1.10-192.168.1.254(1-9留给专用设备的管理地址)
      • 子网掩码:255.255.255.0
      • 网关:192.168.1.1
      • 首选DNS:223.5.5.5
      • 备选DNS:114.114.114.114
    • iKuai+OpenWrt版本:
      • 客户端地址:192.168.1.10-192.168.1.254(1-9留给专用设备的管理地址)
      • 子网掩码:255.255.255.0
      • 网关:192.168.1.3
      • 首选DNS:192.168.1.3
      • 备选DNS:192.168.1.3
  • DNS设置>DNS设置
    • 单iKuai版:
      • 首选DNS:223.5.5.5
      • 备选DNS:114.114.114.114
    • iKuai+OpenWrt版本:
      • 首选DNS:192.168.1.3
      • 备选DNS:192.168.1.3
  • UPnP设置>UPnP设置
    • UPnP即插即用服务:开启
    • 允许内网IP映射:0.0.0.0-255.255.255.255
    • 默认线路设置:任意
    • 掉线检测:开启
    • 检测周期:2
    • 定时重启:开启
    • 重启周期:全选
    • 重启时间:05:00。

6.2   OpenWrt设置

6.2.1   定时重启

系统>定时重启:启用,设置每天05:00。

6.2.2   网络

6.2.2.1   接口>LAN

  • IPv4地址:192.168.1.3。管理OpenWrt的地址。
  • 和ikuai一致保证同一个网段。
  • IPv4网关:指向iKuai的网关。保证OpenWrt的上网数据传输到iKuai再传输到外网。
  • 使用自定义的DNS服务器:223.5.5.5(初始先设置阿里云DNS保证设置过程的上网)。后面等设置好SmartDNS之后改由OpenWrt代理,即输入192.168.1.3。
  • 关闭DHCP服务,统一由iKuai分配。
  • 禁用掉IPv6服务。
  • 物理设置:桥接接口。(因为选用的也是)
  • lan高级设置:不勾选IPv6,勾选【开启开机自动运行】和【强制链路】。

6.2.2.2   Turbo ACC 加速

只开启前三个即可,DNS缓存后面由SmartDNS来管理。

6.2.2.3   防火墙设置

  • SYN-flood 防御:关闭(此项开启关闭都行)
  • 丢弃无效数据包:启用(此项开启关闭都行)
  • 启用FullCone-NAT:高性能模式
  • 入站数据、出站数据、转发都设置为接受。
  • lan口开启:IP 动态伪装(只保留lan口的规格,其他接口全部删除)
  • 防火墙自定义规则(一般默认就有前四条规则,没有需加上)
    1
    2
    3
    4
    5
    iptables -t nat -A PREROUTING -p udp —dport 53 -j REDIRECT —to-ports 53
    iptables -t nat -A PREROUTING -p tcp —dport 53 -j REDIRECT —to-ports 53
    [ -n “$(command -v ip6tables)” ] && ip6tables -t nat -A PREROUTING -p udp —dport 53 -j REDIRECT —to-ports 53
    [ -n “$(command -v ip6tables)” ] && ip6tables -t nat -A PREROUTING -p tcp —dport 53 -j REDIRECT —to-ports 53
    iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE

6.2.3   SmartDNS

Bleach OpenWrt固件自带SmartDNS,eSir GDQ高大全固件需自行手动安装。

  • 基本设置
    • 启用
    • 本地端口6053
  • 高级设置
    • 开启TCP服务器
    • 勾选域名预加载
    • 勾选缓存过期服务
    • 缓存大小设为:1000000
    • 域名TTL最大值设为:3600
  • 第二DNS服务器
    • 启用
    • 本地端口5335
    • TCP服务器
    • 服务器组:oversea
    • 跳过测速
    • 跳过address规则
    • 跳过address SOA(#)规则
    • 跳过双栈优选
    • 跳过cache
    • 停用IPV6地址解析
  • 上游服务器(添加DNS的设置里面可以设置服务器组)
    • 服务器组名称:china(10条左右,包含iKuai路由宽带拨号返回的两个宽带供应商提供的DNS)
    • 服务器组名称:oversea(10条左右)
  • 域名规则
    • 服务器组:china
    • 域名分流设置:跳过测试
  • 自定义设置(只设置两条,其他全部注释掉)
    1
    2
    bind:6053 -group china
    bind:5335 -group oversea
  • 保存&应用

6.2.4   AdGuard Home

6.2.4.1   手动更新内核

  1. 下载最新的AdGuardHome内核(AdGuardHome_linux_amd64.tar.gz):点击跳转
  2. 解压获取「AdGuardHome」文件。
  3. 使用WinSCP登录openwrt虚拟机,进入到路径/usr/bin/AdGuardHome/,上传「AdGuardHome」文件。右击属性,分配权限0755(rwxr-xr-x)。

能科学上网时,点击检查更新,直接更新核心即可。

6.2.4.2   设置AdGuard Home

  • 启用
  • AdGuardHome重定向模式:作为dnsmasq的上游服务器
  • 详细日志
  • 开机后网络准备好时重启
  • 在关机时备份工作目录文件(所有的选项)
  • 打开192.168.1.3:3000地址并配置
  • 网页管理界面>监听接口>所有接口,端口号设置为默认的3000
  • DNS 服务器>监听接口>所有接口,端口号设置为5351
  • 网页管理界面登录密码
  • 设置>常规设置>使用过滤器和Hosts文件以拦截指定域名(其他选项全部关闭)
  • 设置>DNS设置
    • 上游 DNS 服务器
      1
      2
      127.0.0.1:6053
      127.0.0.1:5335
      • 并行请求
      • Bootstrap DNS:127.0.0.1:6053
      • 应用
    • DNS服务配置
      • 速度限制:0
      • DNS 缓存配置:都空着(采用上游DNS服务器,让SmartDNS来管理)
  • 设置>过滤器>DNS黑名单设置(将阻止匹配 DNS 拦截清单的域名):综合性的规则列表启用几个即可。

6.2.5   DNSMASQ设置

打开网络>DHCP/DNS。主要是解除DNSMASQ的DNS功能,只保留转发功能,让DNSMASQ作为AdGuard Home的下级服务生效。

  • DNS转发:127.0.0.1#5351。如果未自动设置成这个,手动强制改成这个。
  • HOSTS 和解析文件:忽略掉解析文件,不用DNSMASQ的解析。
  • 高级设置
    • DNS服务器端口:53
    • DNS查询缓存的大小:0
    • 最大并发查询数:1500
  • 保存&应用

6.2.6   OpenClash

实测,Meta内核因DNS转发问题,导致无法正常上网。例如GOOGLE可能会出现证书异常的问题,YOUTUBE点击视频后会存在一直转圈圈的情况,无法加载视频。现阶段稳定工作的模式为:Fake-IP的普通TUN模式,模型模式可调TUN或者混合。代理模式使用规则上网即可。

6.2.6.1   更新客户端

  1. 下载客户端安装包:点击跳转
  2. 使用WinSCP登录openwrt虚拟机,进入到路径家目录home,上传安装包,运行安装opkg install ./luci-app-openclash_0.46.014-beta_all.ipk。如果报错,尝试opkg update

6.2.6.2   手动更新内核

内核下载地址:

新的内核只支持fake-ip,需要上传Meta内核。

能科学上网时,点击检查并更新,直接更新核心即可。

更新步骤:

  1. 下载内核。
  2. 使用WinSCP登录openwrt虚拟机,进入到路径/etc/openclash/core,上传内核文件。右击属性,分配权限0755(rwxr-xr-x)。

内核对应的名称(压缩包解压出来的名称不一定对应,需手动修改):

  • Dev 内核: clash
  • Tun 内核: clash_tun
  • Meta 内核: clash_meta

6.2.6.3   OpenClash设置

  • 运行状态:启动OPENCLASH
  • 插件设置
    • 模式设置>运行模式:Fake-IP(TUN-混合)模式【UDP-TUN,TCP-转发】
    • DNS设置>本地DNS劫持:停用
    • GEO数据库订阅:设置自动更新
    • 大陆白名单订阅:设置自动更新
  • 配置订阅:设置自动更新,更新间隔60分钟

6.2.7   扩容Overlay软件安装空间

  1. 关闭openwrt虚拟机。
  2. 硬件>硬盘>更多,调整磁盘大小(只能增加大小,按需增加所需硬盘的空间大小)。
  3. 打开openwrt虚拟机。
  4. 系统>磁盘管理L:将新增磁盘大小新建一个分区并格式化为ext4,保存并应用
  5. 挂载刚才新增的分区,举例sda3:mount /dev/sda3 /mnt/sda3,保存并应用。
  6. 拷贝源overlay目录的文件到新分区内:cp -r /overlay/* /mnt/sda3
  7. 系统>挂载点:添加sda3挂载点为「作为外部overlay使用(/overlay)」,保存并应用。
  8. 重启openwrt虚拟机,检查扩容情况。
  9. 根据情况可能需手动移除之前挂载的/mnt/sda3状态。

7   后记

这光猫还是有断流情况,估计是过热导致掉线,后续考虑下加装小风扇。

断流降速故障已查明:虚拟网卡模型设置为Intel E1000导致的故障,重设为virtio半虚拟化即可解决。

8   宽带网络总结

8.1   会话数(连接数)

会话数(连接数)在线测试工具:https://qps.itzmx.com

连接数为应用产生的网络连接数,例如在爱快首页上看到连接数。并发连接数为 qps ,即每秒发起请求,等于在同一秒内,产生了 250 个请求。

会话数(连接数)的限制,一般普通线路带宽会有会话数限制,专线则没有该限制。触发限制的常见现象为:超过并发连接数上限后,此前已建立连接数的软件和网络请求可正常通信,并且期间ping一切正常,延迟没有任何抖动现象,但是新开软件则连不上网络。

9   参考文献

[1] Installation Failing: "Failed to prepare EFI boot using Grub"[EB/OL]. https://forum.proxmox.com/threads/installation-failing-failed-to-prepare-efi-boot-using-grub.122002/.
[2] 软路由科普系列 篇二:PVE安装iKuai OpenWrt 旁路由 基础设置 保姆级全教程[EB/OL]. https://post.smzdm.com/p/awrx4lxm/.
[3] ProxmoxVE 7.4 升级到 8.0,详细步骤[EB/OL]. https://blog.margrop.net/post/pve-7-upgrade-to-8/.
[4] PVE 联网及更换国内源[EB/OL]. https://www.cnblogs.com/pdblogs/p/16218543.html.
[5] Force update from unsigned repository[EB/OL]. https://askubuntu.com/questions/732985/force-update-from-unsigned-repository.
[6] ProxmoxVE(PVE) 启用 IOMMU[EB/OL]. https://www.insilen.com/post/501.html.
[7] Enable IOMMU or VT-d in your motherboard BIOS[EB/OL]. https://us.informatiweb.net/tutorials/it/bios/enable-iommu-or-vt-d-in-your-bios.html.
[8] ProxmoxVE(PVE) 使用 IMG 镜像文件,img 转 qcow2[EB/OL]. https://www.lxtx.tech/index.php/archives/65/.
[9]『软路由踩坑指南』篇三:ESXi 8.0 虚拟机安装 iKuai 主路由及保姆级配置[EB/OL]. https://post.smzdm.com/p/a5op28x7/.
[10]『软路由踩坑指南』篇四:ESXi 8.0 虚拟机安装 openWrt 路由系统终极指南[EB/OL]. https://post.smzdm.com/p/a7ngxeel/.
[11]『软路由踩坑指南』篇五:OpenWrt 旁路由进阶篇 SmartDNS+AdGuardHome 设置 DNS 分流、秒开网页、去广告[EB/OL]. https://post.smzdm.com/p/axz6z7w9/.
[12] [openwrt(x86)] OPenWRT 旁路由 +MosDNS+OpenClash+AdGuard Home 傻瓜配置图文教程[EB/OL]. https://www.right.com.cn/forum/thread-8284982-1-1.html.
[13] OpenWrt 扩容 Overlay 和 Docker 软件安装空间教程(内置硬盘版)附:Samba 网络共享设置[EB/OL]. https://www.right.com.cn/forum/thread-7470757-1-1.html.
[14] [OpenWrt] 使用 OpenClash 科学上网[EB/OL]. http://suyu0925.github.io/blog/2022/07/25/openwrt-openclash/.
[15] Proxmox 软件仓库[EB/OL]. https://mirrors.tuna.tsinghua.edu.cn/help/proxmox/.
[16] 开启直通[EB/OL]. https://skyao.io/learning-pve/docs/pass-through/enable/.
[17] [经验分享] 在线连接数测试网页,简单一键测试宽带并发连接数限制[EB/OL]. https://bbs.ikuai8.com/thread-87196-1-1.html.

1   Python 语法摘记

python在相对路径import导入时,报名前面的小数点.是必要的,python3之后都是严格模式。

2   安装python的踩坑记录

2.1   vscode 无法设置python解释器

原因:安装路径在C:\Program Files,vscode权限不够时会导致无法设置python解释器。

解决方法:安装路径直接按默认路径,不装program路径。

2.2   pip安装某些包异常

情况:如果pip安装某些包一直有异常,反复安装卸载包都无法解决的话,并且确认不是包的问题的话。

原因:python环境异常。

解决方法:重装python。

  1. 直接删除python主安装路径的文件
  2. 运行安装程序repair
  3. 点击卸载,再点击安装(必须要卸载再安装,否则site-packages路径下没有任何包,repair操作不会恢复这个路径的文件)

3   卸载python

3.1   卸载清除已安装的pip包的bat批处理脚本

purge_py311_packages.bat

1
2
3
4
5
6
py -3.11 -m pip freeze > py311_requirements.txt
py -3.11 -m pip uninstall -r py311_requirements.txt -y
del py311_requirements.txt

pause
exit

purge_py310_packages.bat

1
2
3
4
5
6
py -3.10 -m pip freeze > py310_requirements.txt
py -3.10 -m pip uninstall -r py310_requirements.txt -y
del py310_requirements.txt

pause
exit

purge_py39_packages.bat

1
2
3
4
5
6
py -3.9 -m pip freeze > py39_requirements.txt
py -3.9 -m pip uninstall -r py39_requirements.txt -y
del py39_requirements.txt

pause
exit

3.2   清除pip缓存

1
2
3
@echo off
pip cache purge
exit

4   python的包管理

Python包管理是指创建、发布和安装Python包的过程。Python包是一种封装和分发Python代码的方式,以便于其他开发者使用和共享。Python包管理涉及的三个关键组件:PyPI、setuptools和wheel。

4.1   PyPI

Python软件包索引(PyPI)是一个在线存储库,用于发布和查找Python包。PyPI允许开发者上传他们的包,并提供一个中心化的搜索引擎,让其他开发者能够找到并安装这些包。PyPI通过pip(Python包安装器)来安装和管理包。

基本的使用:

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
# 升级pip
python -m pip install --upgrade pip
pip install pip -U
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U

# pip设定清华源(升级 pip 到最新的版本 (>=10.0.0) 后进再进行配置)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# win平台pip缓存路径:\%LocalAppData\%>pip>Cache

# 临时一次性使用镜像源安装包
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package

# 安装包
pip install <package_name>
# 升级包
pip install --upgrade <package_name>
# 卸载包
pip uninstall <package_name>
# 显示包文件信息
pip show --files <package_name>
# 列出已过时的包
pip list --outdated

# 卸载全部包
pip freeze > requirements.txt
pip uninstall -r requirements.txt -y

# 安装全部包
pip install -r requirements.txt --upgrade

4.2   setuptools和wheel

setuptools是一个Python包管理工具,用于创建、构建和发布Python包。setuptools通过提供易于使用的命令行接口和配置文件(如setup.py)来简化包管理过程。通过使用setuptools,开发者可以方便地将他们的代码打包成可分发的格式,如源代码分发(sdist)和wheel分发(bdist_wheel)。

wheel是一种Python分发格式,用于提高安装速度和兼容性。与源代码分发(sdist)相比,wheel分发是预编译的,这意味着它们不需要在安装过程中进行编译。这使得wheel分发在安装速度和跨平台兼容性方面具有优势。

基本使用:

  • 安装setuptools和wheel:pip install setuptools wheel
  • 创建setup.py文件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from setuptools import setup, find_packages

    setup(
    name="my_package",
    version="0.1",
    packages=find_packages(),
    install_requires=[
    "requests",
    ],
    )
  • 构建源代码分发和wheel分发:python setup.py sdist bdist_wheel
  • 安装twine并发布包到PyPI:
    1
    2
    pip install twine
    twine upload dist/*
  • 安装wheel分发的包:pip install dist/my_package-0.1-py3-none-any.whl

5   其他

  • 官方3.9版本最后提供二进制文件下载的是3.9.13版本,之后都是补丁形式。

6   参考文献

[1]【Python 基础】Python 包管理:PyPI、setuptools 与 wheel[EB/OL]. https://blog.csdn.net/qq_33578950/article/details/130297451.

第六章:函数

练习6.1

实参和形参的区别的什么?

实参是函数调用的实际值,是形参的初始值。

练习6.2

请指出下列函数哪个有错误,为什么?应该如何修改这些错误呢?

1
2
3
4
5
6
7
8
(a) int f() {
string s;
// ...
return s;
}
(b) f2(int i) { /* ... */ }
(c) int calc(int v1, int v1) { /* ... */ }
(d) double square (double x) return x * x;

修正如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 应定义函数的返回值为string
(a) string f() {
string s;
// ...
return s;
}
// 无返回值时,应定义函数的返回值为void
(b) void f2(int i) { /* ... */ }
// 任意两个形参都不能同名
(c) int calc(int v1, int v2) { /* ... */ }
// 函数定义构成:返回类型、函数名字、0个或多个形参组成的列表(形参以逗号隔开,位于一对圆括号之内)、函数体(由语句块构成,位于一对花括号之内)。
(d) double square (double x) { return x * x; }

练习6.3

编写你自己的fact函数,上机检查是否正确。

练习6.4

编写一个与用户交互的函数,要求用户输入一个数字,计算生成该数字的阶乘。在main函数中调用该函数。

练习6.5

编写一个函数输出其实参的绝对值。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int abs(int i)
{
return i > 0 ? i : -i;
}

int main()
{
std::cout << abs(-5) << std::endl;
return 0;
}

练习6.6

说明形参、局部变量以及局部静态变量的区别。编写一个函数,同时达到这三种形式。

形参定义在函数形参列表里面;局部变量定义在代码块里面;局部静态变量在程序的执行中,第一次经过对象定义语句时初始化,并且直到程序终止时才被销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 例子
int count_add(int n) // n是形参
{
static int ctr = 0; // ctr 是局部静态变量
ctr += n;
return ctr;
}

int main()
{
for (int i = 0; i != 10; ++i) // i 是局部变量
cout << count_add(i) << endl;

return 0;
}

练习6.7

编写一个函数,当它第一次被调用时返回0,以后每次被调用返回值加1。

1
2
3
4
5
int generate()
{
static int ctr = 0;
return ctr++;
}

练习6.8

编写一个名为Chapter6.h的头文件,令其包含6.1节练习中的函数声明。

练习6.9 : fact.cc | factMain.cc

编写你自己的fact.cc 和factMain.cc ,这两个文件都应该包含上一小节的练习中编写的 Chapter6.h 头文件。通过这些文件,理解你的编译器是如何支持分离式编译的。

练习6.10

编写一个函数,使用指针形参交换两个整数的值。在代码中调用该函数并输出交换后的结果,以此验证函数的正确性。

练习6.11

编写并验证你自己的reset函数,使其作用于引用类型的参数。

练习6.12

改写6.2.1节练习中6.10的程序,使用引用而非指针交换两个整数的值。你觉得哪种方法更易于使用呢?为什么?

引用更易于使用,无论是用法理解还是语法编写层面都简洁一些。

练习6.13

假设 T 是某种类型的名字,说明以下两个函数声明的区别:一个是void f(T), 另一个是 void f(&T)。

  • void f(T) 的参数通过「值传递」,在函数中T是实参的副本,改变T值不会影响到原来的实参值。
  • void f(&T) 的参数通过「引用传递」,在函数中T是实参的引用(即T是实参的别名),T的改变也就是实参的改变。

练习6.14

举一个形参应该是引用类型的例子,再举一个形参不能是引用类型的例子。

例如交换两个整数的函数,形参应该是引用

1
2
3
4
5
6
void swap(int& lhs, int& rhs)
{
int temp = lhs;
lhs = rhs;
rhs = temp;
}

当实参的值是右值时(比如整型常量,用作右值时,使用的是对象的值(内容),见左值和右值),形参不能为引用类型

1
2
3
4
5
6
7
8
9
10
int add(int a, int b)
{
return a + b;
}

int main()
{
int i = add(1,2);
return 0;
}

练习6.15

说明find_char 函数中的三个形参为什么是现在的类型,特别说明为什么s是常量引用而occurs是普通引用?为什么s和occurs是引用类型而c不是?如果令s是普通引用会发生什么情况?如果令occurs是常量引用会发生什么情况?

  • 因为字符串可能很长,因此使用引用避免拷贝;而在函数中我们不希望改变 s 的内容,所以令 s 为常量。
  • occurs 是要传到函数外部的变量,所以使用引用,occurs 的值会改变,所以是普通引用。
  • 因为我们只需要 c 的值,这个实参可能是右值(右值实参无法用于引用形参),所以 c 不能用引用类型。
  • 如果 s 是普通引用,函数中也可能会意外改变原来字符串的内容。
  • occurs 如果是常量引用,那么意味着不能改变它的值,那也就失去意义了。

练习6.16

下面的这个函数虽然合法,但是不算特别有用。指出它的局限性并设法改善。

1
bool is_empty(string& s) { return s.empty(); }

局限性:常量字符串字符串字面值无法作为该函数的实参,比如下面这样调用是非法的:

1
2
3
const string str;
bool flag = is_empty(str); //非法
bool flag = is_empty("hello"); //非法

改善:将这个函数的形参定义为常量引用(对const的引用,并非指引用自身是常量,谨记引用不是对象,是对象的别名)

1
bool is_empty(const string& s) { return s.empty(); }

练习6.17

编写一个函数,判断string对象中是否含有大写字母。编写另一个函数,把string对象全部改写成小写形式。在这两个函数中你使用的形参类型相同吗?为什么?

两个函数的形参不一样。第一个函数使用常量引用,第二个函数使用普通引用。

练习6.18

为下面的函数编写函数声明,从给定的名字中推测函数具备的功能。

  • (a) 名为 compare 的函数,返回布尔值,两个参数都是 matrix 类的引用。
  • (b) 名为 change_val 的函数,返回vector的迭代器,有两个参数:一个是int,另一个是vector的迭代器。
1
2
(a) bool compare(matrix &m1, matrix &m2);
(b) vector<int>::iterator change_val(int, vector<int>::iterator);

练习6.19

假定有如下声明,判断哪个调用合法、哪个调用不合法。对于不合法的函数调用,说明原因。

1
2
3
4
5
6
7
8
double calc(double);
int count(const string &, char);
int sum(vector<int>::iterator, vector<int>::iterator, int);
vector<int> vec(10);
(a) calc(23.4, 55.1);
(b) count("abcda",'a');
(c) calc(66);
(d) sum(vec.begin(), vec.end(), 3.8);
  • (a) 不合法。calc只有一个参数。
  • (b) 合法。
  • (c) 合法。
  • (d) 合法。

练习6.20

引用形参什么时候应该是常量引用?如果形参应该是常量引用,而我们将其设为了普通引用,会发生什么情况?

应该尽量将引用形参设为常量引用,除非有明确的目的是为了改变这个引用变量。如果形参应该是常量引用,而我们将其设为了普通引用,那么常量实参将无法作用于普通引用形参,会出现编译报错。

练习6.21

编写一个函数,令其接受两个参数:一个是int型的数,另一个是int指针。函数比较int的值和指针所指的值,返回较大的那个。在该函数中指针的类型应该是什么?

应该是 const int * 类型。

练习6.22

编写一个函数,令其交换两个int指针。

练习6.23

参考本节介绍的几个print函数,根据理解编写你自己的版本。依次调用每个函数使其输入下面定义的i和j:

1
int i = 0, j[2] = { 0, 1 };

练习6.24

描述下面这个函数的行为。如果代码中存在问题,请指出并改正。

1
2
3
4
5
void print(const int ia[10])
{
for (size_t i = 0; i != 10; ++i)
cout << ia[i] << endl;
}

当数组作为实参的时候,会被自动转换为指向首元素的指针。因此函数形参接受的是一个指针。以上代码实际传递只是指针,会有一个隐患,因此无论是const int ia[3]或者const int ia[255]都没有区别,因为无法实际传递数组的大小。如果要让这个代码成功运行,可以将实参改为数组的引用。更多讨论可参阅Confused about array parameters

1
2
3
4
5
6
// 这里的ia必须要有括号,这样才是声明为指向含有10个整数的数组的引用,没有括号就相当于引用的数组
void print(const int (&ia)[10])
{
for (size_t i = 0; i != 10; ++i)
cout << ia[i] << endl;
}

练习6.25

编写一个main函数,令其接受两个实参。把实参的内容连接成一个string对象并输出出来。

练习6.26

编写一个程序,使其接受本节所示的选项;输出传递给main函数的实参的内容。

练习6.27

编写一个函数,它的参数是initializer_list<int>类型的对象,函数的功能是计算列表中所有元素的和。

练习6.28

在error_msg函数的第二个版本中包含ErrCode类型的参数,其中循环内的elem是什么类型?

elemconst string & 类型。

练习6.29

在范围for循环中使用initializer_list对象时,应该将循环控制变量声明成引用类型吗?为什么?

应该使用常量引用类型。initializer_list 对象中的元素永远都是常量,我们无法修改initializer_list 对象中元素的值。

练习6.30

编译第200页的str_subrange函数,看看你的编译器是如何处理函数中的错误的。

错误信息(Visual Studio 2022 Developer Command Prompt v17.9.5):

“str_subrange”: 函数必须返回值 (cpp(C2561))

clang编译器应该还能提示,参见

Control may reach end of non-void function. // error #2

练习6.31

什么情况下返回的引用无效?什么情况下返回常量的引用无效?

当返回的引用的对象是局部变量时,返回的引用无效;当我们希望返回的对象被修改时,返回常量的引用无效。

练习6.32

下面的函数合法吗?如果合法,说明其功能;如果不合法,修改其中的错误并解释原因。

1
2
3
4
5
6
7
int &get(int *array, int index) { return array[index]; }
int main()
{
int ia[10];
for (int i = 0; i != 10; ++i)
get(ia, i) = i;
}

合法。get 函数根据索引取得数组中的元素的引用。

练习6.33

编写一个递归函数,输出vector对象的内容。

练习6.34

如果factorial 函数的停止条件如下所示,将发生什么情况?

1
if (val != 0)

如果val为正数,从结果上来说没有区别(多乘了个 1); 如果val为负数,那么递归永远不会结束。

通常而言,负数没有阶乘,但这里函数定义是int,可以输入负数。这里是问题所在。

练习6.35

在调用factorial 函数时,为什么我们传入的值是 val-1 而非 val--?

如果传入的值是 val--,那么将会永远传入相同的值来调用该函数,递归将永远不会结束。

练习6.36

编写一个函数声明,使其返回数组的引用并且该数组包含10个string对象。不用使用尾置返回类型、decltype或者类型别名。

1
string (&fun())[10];

练习6.37

为上一题的函数再写三个声明,一个使用类型别名,另一个使用尾置返回类型,最后一个使用decltype关键字。你觉得哪种形式最好?为什么?

1
2
3
4
5
6
7
typedef string str_arr[10];
str_arr& fun();

auto fun()->string(&)[10];

string s[10];
decltype(s)& fun();

我觉得尾置返回类型最好。

练习6.38

修改arrPtr函数,使其返回数组的引用。

1
2
3
4
decltype(odd)& arrPtr(int i)
{
return (i % 2) ? odd : even;
}

练习6.39

说明在下面的每组声明中第二条声明语句是何含义。如果有非法的声明,请指出来。

1
2
3
4
5
6
(a) int calc(int, int);
int calc(const int, const int);
(b) int get();
double get();
(c) int *reset(int *);
double *reset(double *);
  • (a) 非法。因为顶层const 不影响传入函数的对象,所以第二个声明无法与第一个声明区分开来。
  • (b) 非法。对于重载的函数来说,它们应该只有形参的数量和形参的类型不同。返回值与重载无关。
  • (c) 合法。

练习6.40

下面的哪个声明是错误的?为什么?

1
2
(a) int ff(int a, int b = 0, int c = 0);
(b) char *init(int ht = 24, int wd, char bckgrnd);
  • (a) 正确。
  • (b) 错误。因为一旦某个形参被赋予了默认值,那么它后面的所有形参都必须要有默认值。

练习6.41

下面的哪个调用是非法的?为什么?哪个调用虽然合法但显然与程序员的初衷不符?为什么?

1
2
3
4
char *init(int ht, int wd = 80, char bckgrnd = ' ');
(a) init();
(b) init(24,10);
(c) init(14,'*');
  • (a) 非法。第一个参数不是默认参数,最少需要一个实参。
  • (b) 合法。
  • (c) 合法,但与代码设计初衷不符。实参字符 * 被编译器隐式转换成 int 传入给第二个参数。而初衷是要传给第三个参数。

练习6.42

给make_plural函数(参见6.3.2节,第201页)的第二个形参赋予默认实参's', 利用新版本的函数输出单词success和failure的单数和复数形式。

这里原书应该是写错了(英文原版写的也是second),正确应该为「给第三个形参赋予默认实参's'」。

练习6.43

你会把下面的哪个声明和定义放在头文件中?哪个放在源文件中?为什么?

1
2
(a) inline bool eq(const BigInt&, const BigInt&) {...}
(b) void putValues(int *arr, int size);

全部都放在头文件。(a) 是内联函数,(b) 是声明。

练习6.44

将6.2.2节(第189页)的isShorter函数改写成内联函数。

1
2
3
4
inline bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}

练习6.45

回顾在前面的练习中你编写的那些函数,它们应该是内联函数吗?如果是,将它们改写成内联函数;如果不是,说明原因。

一般来说,内联机制用于优化规模小、流程直接、频繁调用的函数。

例如,练习6.38的arrPtr函数和练习6.42的make_plural函数应该定义为内联函数。其他函数若规模不小,且只有一次调用的话,即使定义为内联函数,编译器也很可能会选择忽略,并不会展开内联(内联说明只是向编译器发出的一个请求,实际是否展开内联要取决于编译器)。

练习6.46

能把isShorter函数定义成constexpr函数吗?如果能,将它改写成constexpr函数;如果不能,说明原因。

不能。constexpr函数的返回值类型及所有形参都需要为字面值类型。std::string::size()不是常量函数,s1.size() < s2.size()也不是常量表达式,它的运行结果虽然可以强制转换成常量类型,但由于常量≠常量表达式,即const≠constexpr,因为constexpr关键字不能进行强制类型转换,一个常量可能在运行时才能确定值,而常量表达式要求在编译阶段就确定值。

练习6.47

改写6.3.2节(第205页)练习中使用递归输出vector内容的程序,使其有条件地输出与执行过程有关的信息。例如,每次调用时输出vector对象的大小。分别在打开和关闭调试器的情况下编译并执行这个程序。

练习6.48

说明下面这个循环的含义,它对assert的使用合理吗?

1
2
3
string s;
while (cin >> s && s != sought) { } //空函数体
assert(cin);

不合理。从这个程序的意图来看,应该用

1
assert(s == sought);

练习6.49

什么是候选函数?什么是可行函数?

  • 候选函数:与被调用函数同名,并且其声明在调用点可见。
  • 可行函数:形参与实参的数量相等,并且每个实参类型与对应的形参类型相同或者能转换成形参的类型。

练习6.50

已知有第217页对函数 f 的声明,对于下面的每一个调用列出可行函数。其中哪个函数是最佳匹配?如果调用不合法,是因为没有可匹配的函数还是因为调用具有二义性?

1
2
3
4
(a) f(2.56, 42)
(b) f(42)
(c) f(42, 0)
(d) f(2.56, 3.14)
  • (a) void f(int, int);void f(double, double = 3.14); 是可行函数。该调用具有二义性而不合法。
  • (b) void f(int);void f(double, double = 3.14); 是可行函数。void f(int); 是最佳匹配。
  • (c) void f(int, int);void f(double, double = 3.14); 是可行函数。void f(int, int); 是最佳匹配。
  • (d) void f(int, int);void f(double, double = 3.14); 是可行函数。void f(double, double = 3.14); 是最佳匹配。

练习6.51

编写函数f的4版本,令其各输出一条可以区分的消息。验证上一个练习的答案,如果你的回答错了,反复研究本节内容直到你弄清自己错在何处。

练习6.52

已知有如下声明:

1
2
void manip(int ,int);
double dobj;

请指出下列调用中每个类型转换的等级(参见6.6.1节,第219页)。

1
2
(a) manip('a', 'z');
(b) manip(55.4, dobj);
  • (a) 第3级。类型提升实现的匹配。
  • (b) 第4级。算术类型转换实现的匹配。

练习6.53

说明下列每组声明中的第二条语句会产生什么影响,并指出哪些不合法(如果有的话)。

1
2
3
4
5
6
(a) int calc(int&, int&);
int calc(const int&, const int&);
(b) int calc(char*, char*);
int calc(const char*, const char*);
(c) int calc(char*, char*);
int calc(char* const, char* const);

(c) 不合法。顶层const不影响传入函数的对象。

练习6.54

编写函数的声明,令其接受两个int形参并返回类型也是int;然后声明一个vector对象,令其元素是指向该函数的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int func(int a, int b);

using pFunc1 = decltype(func) *;
typedef decltype(func) *pFunc2;
using pFunc3 = int (*)(int a, int b);
using pFunc4 = int(int a, int b);
typedef int(*pFunc5)(int a, int b);
using pFunc6 = decltype(func);

std::vector<pFunc1> vec1;
std::vector<pFunc2> vec2;
std::vector<pFunc3> vec3;
std::vector<pFunc4*> vec4;
std::vector<pFunc5> vec5;
std::vector<pFunc6*> vec6;

练习6.55

编写4个函数,分别对两个int值执行加、减、乘、除运算;在上一题创建的vector对象中保存指向这些函数的指针。

1
2
3
4
5
6
7
8
9
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }

vec1.push_back(add);
vec1.push_back(subtract);
vec1.push_back(multiply);
vec1.push_back(divide);

练习6.56

调用上述vector对象中的每个元素并输出其结果。

1   练习成果展示

2   前期工作

设计草图,善于利用参考资料(搜索引擎>图片,现实中的实物>拍照等)。三维创作需要时间,很难一蹴而就,慢慢迭代制作即可。

3   建模

开始建模之前,要注意建模物体的尺寸。注意观察物体外形,思考用哪些基础网格mesh创建模型会更容易点。

查看当前物体的尺寸:右边侧边栏(快捷键N)>item条目>尺寸

设置当前场景的单位:属性编辑器>场景属性>单位。

第五章:语句

练习5.1

什么是空语句?什么时候会用到空语句?

只含有一个单独的分号的语句是空语句。如:

1
;

如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。(建议做好注释说明空语句作用)

1
2
3
//重复读入数据直至到达文件末尾或某次输入的值等于sought
while (cin >> s && s != sought)
;

练习5.2

什么是块?什么时候会用到块?

用花括号括起来的语句和声明的序列就是块。

1
2
3
{
// ...
}

如果在程序的某个地方,逻辑上需要多条语句,而语法上只能容纳一条语句,此时应该使用块。

1
2
3
4
while (val <= 10) {
sum += val;
++val;
}

练习5.3

使用逗号运算符重写(参见4.10节,第140页)1.4.1节的 while 循环,使它不再需要块,观察改写之后的代码可读性提高了还是降低了。

1
2
while (val <= 10)
sum += val, ++val;

代码的可读性降低了。

练习5.4

说明下列例子的含义,如果存在问题,试着修改它。

1
2
3
(a) while (string::iterator iter != s.end()) { /* . . . */ }
(b) while (bool status = find(word)) { /* . . . */ }
if (!status) { /* . . . */ }
  • (a) 这个循环试图用迭代器遍历string,但是变量的定义应该放在循环的外面,目前每次循环都会重新定义一个变量,明显是错误的。
  • (b) 这个循环的 while 和 if 是两个独立的语句,if 语句中无法访问 status 变量,正确的做法是应该将 if 语句包含在 while 里面,

练习5.5

写一段自己的程序,使用if else 语句实现把数字转换为字母成绩的要求。

练习5.6

改写上一题的程序,使用条件运算符(参见4.7节,第134页)代替if else语句。

阅读全文 »

R.I.P