PyInstaller打包不同的Python版本程序。

##PyInstaller打包不同的Python版本程序。

因为Python 3.9以后的版本不再支持Windows 7系统,当我们打包的.exe程序需要在Windows 7系统上运行时,必须使用较低版本的Python解释器,如Python 3.8。

我一开始使用Python 3.10打包程序,当生成的.exe程序到Windows7系统上运行时候会报错误:

无法启动此程序,因为计算机中丢失api-ms-win-core-path-l1-1-0.dll

因此,我需要用Python 3.8来打包我程序。但我们在打包时使用的命令:PyInstaller -F。是使用当前系统环境变量中默认的Python解释器版本。

也就是说,当系统环境变量中默认为Python 3.10(已安装了PyInstaller包),那PyInstaller命令打包的程序会默认使用Python 3.10的环境。

我并没有更好的办法,只能新下载一个Python 3.8的解释器,并且将环境变量也设置成Python 3.8。还需要在Python 3.8中安装PyInstaller包。否则PyInstaller -F命令可能会无效或继续使用Python 3.10的解释器。

环境变量中Python 3.8的路径在Python 3.10上方。在终端中执行的Python命令会优先使用Python 3.8的解释器。如果找不到才会去找Python 3.10的解释器。

在系统变量中我使用了Python 3.8的版本,并且安装了PyInstaller包。接下来使用PyInstaller的打包命令:PyInstaller -F main.py。打包后的.exe程序使用的就是系统变量中的Python 3.8版本解释器。

总结:如果你希望你打包的程序使用某个版本的Python解释器,例如Python 3.8版本。那你需要做以下几件事:

  • 1、下载你需要的版本的Python解释器——下载Python 3.8版本的解释器。
  • 2、将系统变量配置成你需要的Python解释器——在命令行(终端)中输入Python后显示的是Python 3.8版本。
  • 3、安装PyInstaller包——PyInstaller必须在Python 3.8的环境中。
  • 4、在你的程序中使用PyInstaller的打包命令——在打包时候显示Python: 3.8。

在打包时显示使用的Python版本就是你程序所使用的Python解释器。

这里我多说两句,通常我们的开发环境和我们使用的操作系统环境是不一样的。我开发的程序使用的是Python 3.10。因为我的IDE指向的是某个固定的解释器,也就是Python 3.10。但我的操作系统环境变量是指向另一个解释器。而终端是依附操作系统的,使用终端打包就容易出现,打包环境和开发环境不一致的情况。

C语言中的二、八、十、十六进制。

##C语言中的二、八、十、十六进制。

我们都知道无论是什么样的值,在计算机中都是以0和1储存的。基于一串0和1组成的数,我们根据我们的定义或规定来表示,才能变成我们想要的“数据”。

下面是我们的输入,我们可以根据我们的需要输入不同进制的数。

#include <stdio.h>
int main()
{
	int a = 0b00010100; // 二进制
	int b = 0000000010;// 八进制
	int c = 1000000000;// 十进制
	int d = 0x00010100; // 十六进制
	printf("%d\n", a); // a为十进制的20
	printf("%d\n", b); // b为十进制的8
	printf("%d\n", c); // c为十进制的1000000000
	printf("%d\n", d); // d为十进制的65792
}

0b开头且0和1组成的数为二进制;0开头且0~7组成的数为八进制;1~9开头且1~9组成的数为十进制;0x开头且0~F组成的数为十六进制。

逻辑运算和位运算——与或非、左移和右移

##逻辑运算和位运算——与或非、左移和右移

与:当且仅当两个数的位都为1时,为真。

或:两个数只要任意一个为1时,就为真。

非:真为假,假为真。


这次重点还是放在左移运算和右移运算上。下面代码的注释简化了int型的字节数。int实际上是4个字节。

左移运算:

#include <stdio.h>
int main()
{
	int a = 0b00010100 << 2; // 左移两位
	printf("%d\n", a); // 二进制0b01010000
}

数据左移时候,最左边位将被丢弃,同时在最右边补0。

右移运算:

#include <stdio.h>
int main()
{
	int a = 0b00010100 >> 2; // 右移两位
	printf("%d\n", a); // 二进制0b00000101

	int b = -10 >> 2; // 十进制为-10,二进制为0b11110110
	printf("%d\n", b); // 十进制为-3,二进制为0b11111101
}

右移的时候,最右边的位将被丢弃,但右移要稍微复杂一些。如果数字是一个无符号数值,则用0填补最左边位。如果数字是一个有符号数值,则用根据符号位填补最左边的位。也就是说,对于无符号数,用0填补最左边的位,丢弃最右边的位;对于有符号数,用1填补最左边的位,丢弃最右边的位。

逻辑运算和位运算——异或

##逻辑运算和位运算——异或

如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。

更通俗的讲就是:两个值不同为真,两个值相同为假。

10
10(假)1(真)
01(真)0(假)
异或运算真值表

上面是异或在逻辑运算中的使用。但如果异或放在位运算中,就有很多的应用场景。


我们把上面的表抽取出来,看成位运算的话可以得到下面两个表,并得出两个简单结论:

10
010
用0进行按位异或运算

结论一:如果一个数用0进行按位异或的话,会得到这个数本身。

10
101
用1进行按位异或运算

结论二:如果一个数用1进行按位异或的话,会得到相反的数。也就是取反。


用上面的两个结论,我们有四种基本的异或使用环境:

使用环境一:使某些特定的位取反。

a = 10100001;
b = 00000110;
c = a ^ b; // 0b10100111

使用环境二:实现两个值的交换,而不必使用临时变量。

a = a ^ b; // a=0b10100111
b = b ^ a; // b=0b10100001
a = a ^ b; // a=0b00000110

使用环境三:将变量自身置零。

a = 10100001;
a = a ^ a; // 0b00000000

使用环境四:快速判断两个值是否相等。

if ((a ^ b) == 0)
{
	//如果a和b相同则继续执行
	...
}

从上面的几个基本使用方法还可以衍生出一些高级的用法。

用法一:如异或的加密运算。

#include <stdio.h>

int main()
{
	char a = 'w'; // 原文
	char secret = '8'; // 密钥

	a = (char)(a ^ secret); // 加密
	printf("%c\n", a); // 大写字母O
	a = (char)(a ^ secret); // 解密
	printf("%c\n", a); // w
}

这里使用了异或取反的特点。用同一个密钥(在上面代码中为变量secret),对相同的位进行异或取反两次就可以得到原文。也就是负负得正的原理,“负”一次为密文,再“负”一次就是原文了。

用法二:检测两个数不同的比特位位置。

int a = 0b10100001;
int b = 0b10101011;
int c = a ^ b; // 0b00001010

根据结果我们可以看到数a和数b,第1位和第4位的比特位是不同的。当且仅当只有一个数的某位上为1时,结果的该位才为1。否则结果的该位为0。

用法三:数a需要改变多少个比特位才能得到数b。

这是上面用法的扩展。

#include <stdio.h>

int main()
{
	int a = 0b10100001;
	int b = 0b10101011;
	int c = a ^ b; // 0b00001010

	int count = 0;
	while (c)
	{
		c &= (c - 1);
		count++;
	}
	printf("%d", count); // count的值为2
}

在上面代码中,数a和数b异或的结果c为0b00001010 。结果c表示,数a需要根据数c,在数c比特位为1的对应位置取反,就可以得到数b。具体到上面的代码中,数a需要在第1位和第4位的位置取反,就可以得到数b。而代码中while循环和其中count的意思是,数a需要改变2个比特位才能得到数b。

C语言中联合union的正确用法。

##C语言中联合union的正确用法。

假设你想用结构来模拟某样东西。但这个东西的属性可以用多种的数据类型来表示。

例如,一箱苹果你可以几种数据类型来表示:

  • 1、一箱有10个苹果。(int)
  • 2、一箱有2.5斤苹果。(float)
  • 3、一箱有1.25公斤苹果。(float)

如果使用常规的结构体来表示的话:

typedef struct {
	int number; // 苹果个数 4字节
	float weight_half_KG; // 苹果重量(斤) 4字节
	float weight_KG; // 苹果重量(公斤) 4字节
}Apple;

这不是一个好主意,因为结构体在内存中占用了更多的空间(结构体Apple占用了12个字节),无论用户是只使用了一个字段,还是三个字段都使用了。用户可能会既设置一箱苹果的数量,也设置了重量。

因此,我们会自然而然的想,如果有一种数据类型,可以根据我给出的数据来决定是使用何种数据类型就好了。当我给出的是整形数据,那就使用“int number”;如果我给出的是浮点数数据,根据需要使用“float weight_half_KG”或“float weight_KG”。而C语言中的联合就可以做到这点。

当定义联合时,计算机会为根据其中最大的字段分配空间。

typedef union {
	int number; // 苹果个数 4字节
	float weight_half_KG; // 苹果重量(斤) 4字节
	float weight_KG; // 苹果重量(公斤) 4字节
}Apple;

在这里三个字段的都是4字节,因此计算机会分配4字节的内存空间给我们定义的联合变量使用。无论是设置了number,还是weight_half_KG,还是weight_KG,他们都会保存在内存中的同一个地方。而且长度只有4个字节。

我们可以使用整形的变量:

int main()
{
	Apple a;
	a.number = 10;  // 一箱苹果有10个
}

我们也可以使用浮点数的变量:

int main()
{
	Apple a;
	a.weight_half_KG= 2.5; // 一箱苹果有2.5斤
}

获取联合成员的方式和获取结构成员的方式一。唯一的差异在于,当改变一个联合成员的值时,实际上修改了该联合所有成员的值。

int main()
{
	Apple a;
	a.weight_KG= 1.25; // 一箱苹果有1.25公斤

	printf("%p\n", &a.number);
	printf("%p\n", &a.weight_half_KG);
	printf("%p\n", &a.weight_KG);
}

联合中的所有字段都可以访问,但它们会指向同一个地址。只是它们在使用时表现的数据类型不一样。

计算机需要保证联合的大小固定。唯一的办法就是使用最大的内存,让任何一个字段都装得下。如果定义的时候没有指定初始化,会被隐式地初始化为0值

联合提供了一种方法,一种可以用不同数据类型创建变量的方法。而且联合经常会和结构体一起使用。

C语言中static关键字的正确用法。

##C语言中static关键字的正确用法。

如果你想要创建一个带有计数功能的函数,你可以这么写:

int count = 0;
int counter()
{
	return ++count;
}

这段代码功能上没什么问题。但它使用了一个叫count的全局变量。count的作用域是全局。这意味着所有函数都可以修改count的值。在一个大型程序中,需要非常小心的控制全局变量的个数,因为它们可能导致代码出错。

但C语言允许你创建只能在函数局部作用域访问的全局变量。

int counter()
{
	static int count = 0;
	return ++count;
}

使用static关键字可以把变量保存在全局量区中(可以当成全局变量),但是当其他函数试图访问count变量时编译器会抛出错误。

如果你调用上面代码中的函数,count的值不会被反复定义(初始化)。

#include <stdio.h>

int main()
{
	int a = 0;
	a = counter(); // 此时a的值为1
	a = counter(); // 此时a的值为2
	printf("%d",a);
}

##用static定义私有变量或函数。

也可以在函数外使用static关键字,它表示“只有这个.c文件中的代码可以使用这个变量(或函数)”。举个例子:

static int days = 365;
static void update_account(int x)
{
	...
}
// 变量days和函数update_account只有当前.c文件可以访问。

static关键字可以用来控制变量或函数的作用域。可以防止其他代码以意想不到的方式访问你的数据或函数。

C语言中的一些小问题。

##C语言中的一些小问题

#头文件的名字用双引号括起来,而不是尖括号,它们的区别是什么?

#include <stdio.h>
#include "totaller.h"

当编译器看到尖括号,就会到标准库代码所在目录查找头文件。如果用引号把文件名括起来,编译器就会在本地查找文件。

但严格来说,这是由编译器的工作方式决定的。通常情况下,引号表示以相对路径查找头文件,如果不加目录名,只包含一个文件名,编译器就会在当前目录下查找头文件;如果用了尖括号,编译器就会以绝对路径查找头文件。

gcc知道标准库的绝对路径。如果你在Windows中安装了MinGW编译器,那标准库头文件stdio.h通常就在安装目录下C:\MinGW\include\stdio.h中。

#什么是预处理?

预处理是在正式编译开始之前修改代码,创建新的源文件。例如预处理会读取头文件中的内容,插入主文件。但预处理并不会真的创建一个文件。为了提高编译的效率,编译器通常会用管道在两个阶段之间发送数据。

#为什么C语言中是使用复制的方式把值给形参?

因为计算机是通过赋值的方式向函数传递值的。所有赋值都会复制值。

字符集整理。

##字符集整理

ASCII

最通用的信息交换标准,等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符。

标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符。

ANSI

通常使用 0x00~0x7f 范围的1 个字节来表示 1 个英文字符。超出此范围的使用0x80~0xFFFF来编码。不同ANSI编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段ANSI编码的文本中。

Unicode

将世界上所有的文字用2个字节(也就是16位)统一进行编码。像这样统一编码,2个字节就已经足够容纳世界上所有的语言的大部分文字了。

UTF-8

UTF-16

GBK

GB2312

其中GB的意思其实是“国家标准”。

一些计算机的冷知识问答

##一些计算机的冷知识问答

#为什么数组的下标从0开始编号?为什么不是1?

索引值是一个偏移量:它表示当前要引用的这个元素到数组中第一个字符之间的距离。计算机在存储器中以连续字节的形式保存数组,并利用索引计算数组元素在存储器中的位置。这样可以根据下标很快找到想要的数据。

#在C语言中,布尔值是用数字表示的。对C语言来讲,数字0代表假的值。任何不等于0的数字都将被当作真处理。

#在C语言中,逻辑与和逻辑或的运算可以只使用“|”和“&”。而“&&”和“||”被称为短路与和短路或。短路或和短路与表示如果条件成立则执行之后的逻辑。而“|”和“&”也经常被拿来进行逐位运算,对逻辑表达式求值只是它们的其中一个用处。“&”也还被用取内存地址。

#在C语言中,几乎每样东西都有返回值。不仅仅是函数调用,就连赋值表达式也有返回值。

x = 4;
y = (x = 4);
y = x = 4;

我们将4赋值给变量x。但其实“x = 4;”本身也有一个值,这个值就是赋给x的值,也就是4。而“y = (x = 4);”表示返回的值赋给了变量y。当我们把这条语句的括号去掉之后就变成了同时给多个变量赋值的链式赋值语法“y = x = 4;”

#1990年1月15日,AT&T的长途电话系统死机,照成6万人无法使用电话服务。起因是一个负责写电路交换部分C代码的开发人员企图用break从if语句中退出。break使程序跳过了整段代码,引起了这个bug,令七千万次电话呼叫在9个多小时内无法接通。

#在C语言中scanf()的意思其实是scan formatted,它用来扫描带格式的输入。

#声明是一段代码,它声称某样东西(变量或函数)存在;而定义说明了它是说明东西。如果在声明了变量的同时将其设为某个值(例如int x = 4;),这段代码既是声明又是定义。

#为什么C语言中make工具规定必须使用tab缩进,而不是空格缩进呢?

因为make之父Stuart Feldman曾说过:程序正确运行了,于是就保留了下来。几个星期以后,make拥有了几个用户,大部分是朋友,但又不愿破坏代码的基本结构。

#Java的包java.lang中的lang是什么意思?

lang是language的意思,表示Java语言包。Java语言的基本功能都在里面。包括了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等。每个Java程序运行时,系统都会自动地引入java.lang包,所以这个包的加载是缺省的。

#Windows XP的XP是什么意思?

Windows XP中XP代表的就是 Experience(体验)。和Office XP末尾的XP一样

编程语言的超集和语法扩展。

##编程语言的超集和语法扩展

微软开发了一款编程语言叫TypeScript。对于这款语言的介绍中写道:TypeScript是JavaScript的超集,扩展了JavaScript的语法,因此现有的JavaScript代码可与TypeScript一起工作无需任何修改。

超集的定义是:如果一个集合S2中的每一个元素都在集合S1中,且集合S1中可能包含S2中没有的元素,则集合S1就是S2的一个超集,相对的,S2是S1的子集。如果S1是S2的超集,且S1中一定有S2中没有的元素,则S1是S2的真超集,反过来S2是S1的真子集 。

我们再看回编程语言TypeScript。TypeScript是JavaScript的超集,也就是说,TypeScript在JavaScript的基础上可以使用更多的语法。也就是对JavaScript进行了语法扩展。

我们举个简单的例子。假设有款编程语言A只有加法。如果使用多次的加法运算会非常麻烦。于是我们开发了一款新的编程语言B。我们在编程语言A的基础上新增了乘法,使多次的加法运算更加方便。在不改变编程语言A的基础上,我们扩展了乘法运算。这就是对编程语言A的语法扩展,也可以说编程语言B是编程语言A的超集。

其实现在不只出现了针对JavaScript语法扩展的TypeScript。还有对CSS的语法扩展的Less(Leaner Style Sheets)语言。甚至还有其他编程语言的语法扩展。

这些扩展语言通常会使用和基础语言类似的语法。最后将源代码转换为基础语言的语法结构。我们再回到上面编程语言A和编程语言B的例子。我们在源代码中使用编程语言B的乘法运算后,需要将源代码转化为编程语言A的加法运算。这个过程通常由编程语言B使用自己的命令完成,就像源代码编译那样。也就是说,我们写的乘法运算最后会被转换成加法运算。在这里我们也称编程语言A是编程语言B的超集。

这里需要注意的是,超集的定义是:集合S2中的每一个元素都在集合S1中。也就是说扩展语言是向后兼容的。编程语言A中的所有特性都应该可以在编程语言B中使用。对于C++和C语言,我们通常认为C++是对C语言的扩展,但其实并非如此。C语言中有些特性是无法在C++中使用的。

例如在C和C++中,都可以在一个结构的内部声明另一个结构。

struct box
{
    struct point {int x; int y;} upperleft;
    struct point lowerright;
};

struct box ad;      // C和C++都可以
struct point dot;   // C可以,C++报错
box::point dot;     // C报错,C++可以

在C语言中,随后可以使用任意使用这些结构,但是在C++中使用嵌套结构时要使用冒号“:”。例如这样在C语言中可以使用,但C++中无法使用的例子还有很多。我们只能认为C++和C语言是两种具有相似特性的不同语言。或者像经常看到的那样说C++是对C语言做了改进。但我们不能说C++是C语言的超集。说C++是对C语言的语法扩展就更加不合理了。

在无法改变基础语言的情况下新增一些方便好用的语法特性是语法扩展的好处。通常这些语法扩展会设计得方便易学,至少不会和基础语言相差太远。缺点可能就是需要花更多的学习和维护成本了。