编辑推荐: |
本文介绍了Python解释器、C语法语法及C语言可执行文件的内存模型,希望对您的学习有所帮助
。
本文来自于CSDN,由火龙果软件Alice编辑、推荐。 |
|
前言
曾几何时,对Python的源码开始好奇,随着对Python的依赖越来越深,特别想知道guido是怎么在短时间内设计出了这么牛逼的语言。
工作中,不只听到一个人在吐槽Python"速度慢",但是你真的知道Python慢的原因吗?
很多人都说Python是一门解释型语言,是一门脚本语言,不需要编译就直接执行,真的是这样吗?
还有人在不使用Python多线程的情况下依然吐槽GIL。。。。
这种类似的疑问还有很多,只有揭开CPython解释器的神秘面纱才可能知道这些问题的答案,如果你想用好Python,成为Python的高级玩家,那么CPython是你跨不过去的一道门槛。
Python和CPython
Python
python是一门语言
CPython
使用C语言实现的Python解释器,解释器分为两部分,编译器和虚拟机,用Python语言编写完源代码之后,会使用编译器先把python语言编译成字节码,也就是通常我们看到的.pyc或者.pyd文件,虚拟机执行编译好的字节码。
环境介绍
[root@localhost c_source]
Linux localhost.localdomain 3.10.0-1160.71.1.el7.x86_64
[root@localhost c_source]
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-9/root/ usr/libexec/gcc/x86_64-redhat-linux/9/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap -- enable-languages=c,c++,fortran,lto --prefix=/opt /rh/devtoolset-9/root/usr --mandir=/opt/rh/devtoolset-9 /root/usr/share/man --infodir=/opt/rh/ devtoolset-9/root/usr/share/info --with-bugurl= http://bugzilla.redhat.com/bugzilla --enable-shared -- enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --with-default- libstdcxx-abi=gcc4-compatible --enable-plugin --enable-initfini-array --with-isl=/builddir/build/BUILD/gcc-9.3.1-20200408/ obj-x86_64-redhat-linux/isl-install --disable-libmpx --enable-gnu-indirect-function --with-tune= generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 9.3.1 20200408 (Red Hat 9.3.1-2) (GCC)
|
1 简述Python解释器
Python是一门”解释型语言“,python解释器常用的有三种,分别是C语言实现的CPython(官方版本),Java语言实现的Jython,Python语言自己实现的解释器。我们只关注官方版本:用C语言实现的解释器。
CPython 分为两部分,分别是编译器和虚拟机,只是Python将其归结到一条指令下,当你在命令行敲下Python
xxx.py 的时候,CPython解释器会执行两个步骤,先把xxx.py编译成字节码,然后虚拟机在逐条执行字节码。
编译器类似于Javac命令,将xxx.java 编译成xxx.class文件
虚拟机类似于Java命令,将java xxx 会执行编译好的文件
CPython解释器完全用C语言实现的,大部分都是用的C语言的基础语法。下面逐个说明一下用法。
2 C语法语法
2.1 结构体
结构体是C语言中最重要的数据类型,可以让用户自定义数据类型。
定义结构体
语法
struct(关键字) 结构体名 {结构体内容};
struct Student {
char* name;
int age;
}
|
初始化结构体
#include <stdio.h>
#include <stdlib.h>
struct Student {
char *name;
int age;
}stu1, stu2 ;
int main() {
stu1.name = "张三";
stu1.age = 11;
stu2.name = "李四";
stu2.age = 12;
printf("学生1的名字是:%s,年龄是:%d\n", stu1.name, stu1.age);
printf("学生2的名字是:%s,年龄是:%d\n",stu2.name,stu2.age);
return 0;
}
|
2.2 typedef
typedef 关键字用于给数据类型定义别名,常用于给结构体定义别名;
基础应用
#include <stdio.h>
#include <stdlib.h>
int main(){
typedef int integer;
typedef int* pinteger;
integer a = 10;
printf("a=%d,sizeof(a)=%d\n", a, sizeof(a));
integer b = 11;
pinteger pb = &b;
printf("b=%d,&b=%p,pb=%p,*pd=%d\n", b,&b,pb,*pb);
return 0;
}
|
从运行结果看,可以发现integer类型变量与int类型变量相同。
常规应用
typedef 最常用的就是定义结构体的别名
#include <stdio.h>
#include <stdlib.h>
struct Student {
char* name;
int age;
};
int main(){
typedef struct Student stu;
typedef struct Student* pstu;
stu stu1;
stu1.name = "张三";
stu1.age = 11;
pstu stu2 = (pstu)malloc(sizeof(stu));
stu2->name = "李四";
stu2->age = 12;
printf("stu1的名字是:%s,年龄是:%d\n",stu1.name,stu1.age);
printf("stu2的名字是:%s, 年龄是:%d\n",stu2->name, stu2->age);
printf("stu2的地址是: %p\n",stu2);
if(stu2 != NULL){
free(stu2);
}
return 0;
}
|
2.3 宏定义
宏定义用于定义常量和表达式
宏定义一个常量,代码中的符号都会被替换成100
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
int main(){
int sum;
for(int i=0;i<=MAX;i++){
sum += i;
}
printf("sum=%d\n",sum);
return 0;
}
|
预编译
gcc -E t4.c -o t4.i
# 6 "t4.c"
int main(){
int sum;
for(int i=0;i<=100;i++){
sum += i;
}
printf("sum=%d\n",sum);
return 0;
}
|
定义表达式宏
#include <stdio.h>
#include <stdlib.h>
#define ADD(x,y) ((x)+(y))
int main(){
int a = 1;
int b = 2;
int add = ADD(a,b);
printf("add=%d\n", add);
return 0;
}
|
预编译gcc -E t5.c -o t5.i
# 5 "t5.c"
int main(){
int a = 1;
int b = 2;
int add = ((a)+(b));
printf("add=%d\n", add);
return 0;
}
|
加括号的原因是为了在替换的过程中避免因为运算符的原因导致结果与实际不符。
#define MUL(x,y) ((x)*(y))
如果使用上面的宏定义
在代码中这样使用:
MUL(1+2,3+4)
如果加了括号,完成预编译,替换宏之后 ((1+2)* (3+4))
如果不加括号,完成预编译,替换宏之后 (1+2*3+4)
两个结果是完成不一样的。
|
在python的源码中大量使用了宏定义来提高代码的效率
python 3.8.0 中
object.h
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
|
2.4 预编译指令
语法
#if
#ifdef
#ifndef
#else
#elif
#endif
#define
#undef
#defined
|
2.4.1 C语言源码到可执行文件的过程
组成程序的每个源文件通过编译过程分别转换成目标代码。
每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且他可以搜索程序员个人的程序库,将其需要的函数也链接到程序。
预编译 main.c -----> main.i
文本操作
#include 头文件的包含
注释删除:使用空格替换注释
#define 替换,所以宏无法进行调试。
编译 main.i ----> main.s
把c语言代码翻译成汇编代码
语法分析
词法分析
语义分析
符号汇总
汇编 main.s ----> main.o
把汇编代码转换成二进制代码(指令)。
形成符号表。(符号+地址)
链接 main.o ----> main.exe
可执行文件
2.4.2 C语言预定义符号
__FILE__
__LINE__
__DATE__
__TIME__
__STDC__
|
# 4 "t6.c"
int main(){
printf("__FILE__:%s\n","t6.c");
printf("__LINE__:%s\n",6);
printf("__DATE__:%s\n","Nov 7 2022");
printf("__TIME__:%s\n","14:21:47");
printf("__STDC__:%s\n",1);
return 0;
}
|
2.4.3 宏定义的高级玩法
#define NAME "kobe"
#undef NAME
|
# 的作用
使用#,把一个宏参数变成对应的字符串。
#define PRINT(X) printf("the value of "#X" is %d\n", X)
int main()
{
int a = 10;
int b = 20;
PRINT(a);
PRINT(b);
return 0;
}
|
##的作用
## 可以把位于他两边的符号合成一个符号,允许宏定义从分离的文本片段创建创建标识符。
#define CAT(X,Y) X##Y
int main()
{
int class84 = 2021;
printf("%d\n", CAT(class, 84));
}
|
宏与函数的区别
宏和函数的对比
对于上述的宏,也可以用函数实现其功能。
使用宏的优点:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
2.函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之,这个宏可以用于整型、长整型、浮点数等等,宏是类型无关的。
使用宏的缺点:
1.每次调用宏,一份宏定义的代码插入程序中,除非宏比较短,否则可能会大幅度增加代码的长度。
2.宏无法调试。在预编译(预处理)阶段,已经把 # define 给替换了,已经不再是宏了。
3.宏由于类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,更容易导致程序出错。
2.4.4 基础预编译指令#if
#if 表达式
printf("编译\n");
#endif
|
#if 表达式
printf("编译\n");
#else
printf("未编译\n");
#endif
|
案例
#include <stdio.h>
#include <stdlib.h>
int main(){
#if 1
printf("我被编译了\n");
#endif
return 0;
}
|
案例
#include <stdio.h>
#include <stdlib.h>
#define DEBUG 1
int main(){
#ifdef DEBUG
printf("我被编译了!\n");
#endif
return 0;
}
|
2.4.5 判断是否定义宏指令#ifdef
#define DEBUF
#ifdef DEBUG
printf("debug\n");
#endif
|
2.4.6 避免多次包含
a.h
#ifdef __A_H__
#define __A_H__
int add(int x, int y){
return x+y;
}
#endif
|
b.h
#ifndef __B_H__
#define __B_H__
#include "a.h"
int add(int x, int y){
return (x+y) *2;
}
#endif
|
t9.c
#include <stdio.h>
#include <stdlib.h>
#include "b.h"
#include "a.h"
int main(){
int a = 1;
int b = 2;
int result = add(a, b);
printf("result=%d\n", result);
return 0;
}
|
先看这个输出结果
显然用的是b.h里面的add函数,分析一下原因。
t9.c中:
先导入b.h
b.h又导入了a.h
后面是add函数,所以a.h中的函数被b.h的覆盖掉了
如果不使用条件编译#ifdef ,程序会因为重复变量报错的。
注意:使用include导入的时候要注意:内置标准库使用#include <stdio.h>
如果是自己定义的头文件,使用#include “test.h” 使用""编译器会在当前目录中查找
2.5 assert断言
巧用断言是C高级程序员必备的技能,用好断言可以让C程序更加简洁。
不使用断言的程序
#include <stdio.h>
#include <stdlib.h>
float division(float a, float b){
if(b==0){
printf("除数不能为0\n");
exit(-1);
}
return a/b;
}
int main(){
float a = 1.0;
float b = 0;
float res = division(a, b);
return 0;
}
|
使用断言的程序
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
float division(float a, float b){
assert(b != 0);
return a/b;
}
int main(){
float a = 1.0;
float b = 0;
float res = division(a, b);
return 0;
}
|
断言b不等于0失败了!!!
2.6 goto 语法
无条件跳转
语法
goto label;
label:
expressions;
|
注意goto语句不能跨函数使用
goto语句最好的使用场景是跳出多层循环(3层以上的循环)
如果不是非必要的,尽量少使用。
3 C语言可执行文件的内存模型 3.1 代码区text
主要存放编译好的代码和常量,这部分都是不可以修改的。
#include <stdio.h>
void main(){
char *s = "hello world\n";
printf("%p------> %s\n", s, s);
return;
}
|
#include <stdio.h>
void main(){
char *s = "hello world...\n";
printf("%p------> %s\n", s, s);
return;
}
|
从结果可以看出来,给字符串增加了3个字符,代码区就增加了三个字符,很好理解,与预期结果相同。
尝试修改一下常量的值
#include <stdio.h>
void main(){
char *s = "hello world...\n";
printf("%p------> %s\n", s, s);
printf("%c\n",*s);
*s = 'H';
return;
}
~
|
因为常量不能修改,所以报错。
3.2 初始化数据区
初始化数据区(data)负责存储已初始化的全局变量和静态变量,它也是和可执行文件具有相同的生命周期,在程序执行结束之前我们都可以使用它。
上面的例子我们定义的字符串在代码区,那么如果我想修改字符串应该怎么办呢?
我们可以把字符串设置成静态变量。
#include <stdio.h>
void main(){
char *s = "hello world...";
printf("代码区%p------> %s\n", s, s);
char s1[] = "hello world...";
printf("%p------>%s\n", s1, s1);
s1[0] = 'H';
printf("%p------>%s\n",s1, s1);
return;
}
|
3.3 未初始化数据区
最后是未初始化数据区 bss,它是负责存储未初始化全局变量、未初始化静态变量。
#include <stdio.h>
#include <stdlib.h>
int main(){
static int a,b,c,d;
a = 1;
c = 2;
printf("a=%p---%d\n",&a, a);
printf("b=%p---%d\n",&b, b);
printf("c=%p---%d\n",&c, c);
printf("d=%p---%d\n",&d, d);
return 0;
}
|
#include <stdio.h>
#include <stdlib.h>
int main(){
static int a,b,c,d,e;
a = 1;
c = 2;
printf("a=%p---%d\n",&a, a);
printf("b=%p---%d\n",&b, b);
printf("c=%p---%d\n",&c, c);
printf("d=%p---%d\n",&d, d);
printf("e=%p---%d\n",&e, e);
return 0;
}
|
3.4 堆区
栈的效率虽然很高,不用我们维护,但它的局限性也显而易见,就是它要求变量的大小必须明确、固定。
而当我们需要动态大小的内存时,只能使用堆,比如我们要实现可变长度的数组,那么必须分配在堆上,否则无法扩容。而堆上分配内存时,一般都会预留一些空间,这是最佳实践。
在堆上分配内存除了可以让大小动态化,还可以让生命周期动态化。我们说过,函数调用结束之后,那么函数对应的栈帧会被回收,同时相关变量对应的内存也会被回收。所以栈上内存的生命周期是不受开发者控制的,并且局限在当前调用栈。
而堆则不同,堆上分配出来的每一块内存都需要显式地释放,这就使得堆内存有更加灵活的生命周期,可以在不同的调用栈之间共享数据。因为数据只要我们不回收,那么就始终就驻留在堆上,并且何时回收也是由我们来决定的。
因此当内存动态可变的时候,我们会在堆上分配。当然啦,堆内存是负责具体存储数据的,然后还要在栈上分配一个指针,它引用堆区的内存。
3.5 栈区
栈是程序运行的基础,每当一个函数被调用时,一块连续的内存就会在栈顶被分配出来,供函数执行使用,这块内存被称为栈帧(stack
frame),或者简称为帧。
这里我们就又得出了一个结论:在编译时,如果局部变量的大小不确定、或者大小可以改变,那么它的值就无法安全地放在栈上,应该要放在堆上。也就是在堆上为变量分配内存,并且还要在栈上分配一个指针,引用堆上的内存。
因此变量的值究竟分配在栈上还是分配是在堆上,结论如下:
1)如果一个函数返回了局部变量的指针,那么要在堆上为其分配内存;
2)如果在编译时,局部变量的大小不确定、或者大小可以改变,那么它的值就无法安全地放在栈上,所以此时也要在堆上为其分配内存;
3)如果变量的值过大,那么优先在堆上为其分配内存;
|