python和c++的一些区别(一)
1最近使用python编写一个程序,遇到了导入包和参数引入的问题,后知后觉地发现c++和python关于变量存储和对象使用上的区别。
变量存储的机制上
众所周知,python没有c/c++里类似i++和++i这样语法。c/c++中,用户可以使用i++和++i进行整型变量的自增和数据结构的迭代器的自增。而i++和++i的区别在于,i++会返回一个自增前变量的拷贝副本,这个副本被存放在一个新的位置,而自增后的变量数值会被放回原来的地址,副本和变量本身无直接关系。
xxxxxxxxxx
1// c++ 20
2int main(){
3 int i = 0;
4 cout << &i << endl; // 0x61fe08
5 int i2 = i++;
6 cout << &i2 << endl; // 0x61fe04
7 cout << &i << endl; // 0x61fe08
8 return 0;
9}
与c++不同,python中,所有一切都被视为对象,且分为不可变对象和可变对象。
1.可变对象:大多数的容器类,包括列表、字典、集合和用户定义的对象。
2.不可变对象:包括整数、浮点数、字符串、元组和冻结集合。
可变对象很好理解,可以在对象元素的对应位置修改内容,而不具体改变对象所在地址,即不需要创建新的对象。实际上,python的列表对象就就是由一堆指向元素的指针和一些其他功能函数组成。需要注意的是,由于python对内存显式控制不敏感,python中的一个int对象所占空间会远大于在c中的int变量,其他的一些基本类型也是如此。以后可能要专门开一篇挖掘下。
不可变对象就比较麻烦,因为它本身不可被修改的特性,使得在对它进行任何“修改”操作时,进行得不是实际得修改,而是重新绑定,这与C++中的移动语义有些相似。这使得虽然看上去能对对象进行切片操作,但实际上不能修改。于是,你会在python见到如此场景。
xxxxxxxxxx
61# python 3.10.6
2i1,i2 = 1,1
3if i1 == i2: print("i1 == i2") # i1 == i2
4if i1 is i2: print("i1 is i2") # i1 is i2
5print(id(i1)) #0x214468a00f0
6print(id(i2)) #0x214468a00f0
与c中每个变量分配一个地址不同,i1和i2是分别注册的整型变量,两者却地址相同。这就是python设计不可变对象的初衷之一,使用对象缓存优化对常量进行保存,包括小整数缓存(CPython中规定范围是-5至256,但实际会远超这个范围)、字符串驻留、函数默认参数缓存、方法解析缓存等等。因为它们常常会在程序中频繁被使用,使用该方法可以减小内存占用。值得注意的是,True、False、None(尺寸:28B、24B、16B)这些常用布尔值也是有被缓存的,在python程序以单例模式存在。
xxxxxxxxxx
81# python 3.10.6
2i1,i2 = 1,1
3print(i1 is i2) # True
4print(id(i1) == id(1)) # True
5s1,s2 = "hello","hello"
6print(s1 is s2) # True
7# 相当于直接提供了一些较短字符串的右值引用
8print(id(s1) == id("hello")) # True
插一句,python里0,1两个常量对于False,True是特殊的,0,1两个常量可被直接视作布尔值0,1,可以被直接用在判断句子里,而其他数据或者数据结构均不可。其他数据或数据结构,未初始化时布尔值固定为0(False),初始化(not None)后,布尔值为1(True),这点与C++相同。
x
1# python 3.10.6
2print(True is bool(1)) # True
3print(False is bool(0))# True
4print(None == False) # False
5print(bool(None) == False) # True
6print(0 == False,1 == True) # True True
7print(100 == True,[1,2] == True) # False False
设计不可变对象的还有一个好处是安全性。在普通的拷贝对象(值传递)时,原对象和拷贝对象可以“指向”同一个地址,减少内存占用。而在其中一个对象改变时,保留另一个对象的指向不变,创建一个新的对象存储,不需要担心其他绑定在不可变对象上的其他对象被修改。相比之下,C/C++中,每次拷贝数据结构或对象时,拷贝对象(数据结构)都会存储在一个新的地址,会占用更多的空间。不过python的这种转移是隐性的,有时会造成一些问题。例如自增时,对象地址会不断变化,后面需要判断一个变量是否是原来的那个变量;向函数传递变量时,如果传入的实参是不可变对象,那么在函数内对该变量的写入操作都只会作用在函数内创建的局部变量上。
python与c/c++在变量自增下存储区别
xxxxxxxxxx
1# python 3.10.6
2i1 = 1
3i2 = i1
4print(i1,i2)
5print(hex(id(i1)),hex(id(i2)))
6i1+=1
7print(i1,i2)
8print(hex(id(i1)),hex(id(i2)))
9
10# 结果,不可变对象被拷贝后,只是增加一个对原地址上增加一个引用,c/c++同样可以用引用实现
111 1
120x216df2500f0 0x216df2500f0
132 1
140x216df250110 0x216df2500f0
xxxxxxxxxx
1// c++ 20
2class A{
3public:
4 int data = 1;
5};
6int main(){
7 A a1 = A(); // 同python拷贝一个对象
8 A a2 = a1;
9 cout << &a1 <<" " << &a2 <<endl;
10 cout << a1.data << " " << a2.data << endl;
11 a1.data ++;
12 cout << &a1 <<" " << &a2 <<endl;
13 cout << a1.data << " " << a2.data << endl;
14 return 0;
15}
16
17'''结果,对象被拷贝后,新建立一个地址,其下成员变量(非引用)并没有被拷贝
180x55845ff75c 0x55845ff758
191 1
200x55845ff75c 0x55845ff758
212 1
c++和python支持将字面量、自定义对象、全局对象作为占位的默认参数。其中,有一种“默认参数陷阱”也是值得注意的。python中,在默认参数未修改前,其指向全局变量空间/静态变量空间,故参数初始化的会被放在静态变量空间中,而不是栈空间。如果是不可变对象那很ok,但如果是可变对象,会导致对其的写入操作在函数退出后会被保存,这种隐式操作常常会导致预料外的结果,既不报错,也不停止。这种函数设计也是不符合python规范的。
xxxxxxxxxx
1# python 3.10.6
2def fuc(arg1=0,arg2=[]):
3 print("fuc arg1=",arg1)
4 print("fuc id(arg1)=",hex(id(arg1)))
5 arg2.append(arg1)
6 print("fuc arg2=",arg2)
7 print("fuc id(arg2)=",hex(id(arg2)))
8if __name__ == "__main__":
9 fuc(1)
10 print("outside id(1)=",hex(id(1)))
11 fuc(2)
12 fuc(3)
13
14'''结果
15fuc arg1= 1
16fuc id(arg1)= 0x2b14c8600f0
17fuc arg2= [1]
18fuc id(arg2)= 0x2b14cc72a80
19outside id(1)= 0x2b14c8600f0 #如果没有写入新数据,arg1指向静态变量区的1
20fuc arg1= 2
21fuc id(arg1)= 0x2b14c860110
22fuc arg2= [1, 2]
23fuc id(arg2)= 0x2b14cc72a80
24fuc arg1= 3
25fuc id(arg1)= 0x2b14c860130
26fuc arg2= [1, 2, 3]
27fuc id(arg2)= 0x2b14cc72a80 #每次调用fuc里的arg2指向同一个位置
28 #说明arg2并非存放在函数栈区
29 #而存放在静态变量区(static)
30'''
31# 对于自定义对象也是一样
32class Data:
33 def __init__(self,data):
34 self._data = data
35 def getdata(self):
36 return self._data
37 def changedata(self,data):
38 self._data = data
39
40def change(to,data = Data(10)):
41 print(data.getdata())
42 data.changedata(to)
43 print(data.getdata())
44
45if __name__ == "__main__":
46 change(20)
47 change(30)
48'''结果
4910
5020
5120 # 没有重新申请对象
5230
53'''
C++中默认参数可以是NULL、nullptr、字面量、字符串、自定义对象(可见的变量)。但需要注意基本数组和字符串作为默认参数时,数组在作为参数时会退化成指针,无法将数组字面量传给指针,字符串则是不能进行修改。如果要使用数组和字符串作为默认参数,最好使用stl容器array、vector、string等或者自定义容器。
xxxxxxxxxx
311// c++ 20
2void change(int to,int array[5] = {0}){ // 错误示例,字面量直接作为默认参数
3 static int flag = 0; // 代替位置输入
4 array[flag] = to;
5 for (int i = 0;i< 5 ;++i){cout << array[i] << " ";}
6 cout << endl;
7 if(flag < 4){++flag;}
8}
9int main(){
10 change(10);change(11);change(12);
11 return 0;
12}
13// 结果:未定义行为!!!!不要这么干
14
15// 正确示例
16int Oarray[5] = {0};
17void change(int to,int array[5] = Oarray){ // 全局位置的数组作为默认参数,指针传给指针
18 static int x = 0; // 代替位置输入
19 array[x] = to;
20 for(int i = 0;i<5;++i){cout << array[i] << " ";}
21 cout << endl;
22 if(x < 4){++x;}
23}
24int main(){
25 change(10);change(11);change(12);
26 return 0;
27}
28// 结果
2910 0 0 0 0
3010 11 0 0 0
3110 11 12 0 0
32
33// 先声明占位,在初始化在全局/静态位置也是ok的
34void change(int to, int array[] = nullptr) { // 默认为 nullptr
35 static int defaultArray[5] = {0}; // 静态数组,只初始化一次
36 static int flag = 0;
37 int* targetArray = (array == nullptr) ? defaultArray : array; // 如果用户没传数组,就使用默认数组
38 targetArray[flag] = to;
39 for (int i = 0; i < 5; ++i) {cout << targetArray[i] << " ";}
40 cout << endl;
41 if (flag < 4) {++flag;}
42}
43int main() {
44 change(10); change(11);change(12); // 使用默认数组
45 int myArray[5] = {0}; // 也可以传入自己的数组
46 change(99, myArray);
47 return 0;
48}
python在类的继承和多态方面与c/c++中又有许多不同,这也是因为python的设计哲学更加重视“方法实现”,而不在意“对象类型”,不过留到下次再写吧。