python和c++的一些区别(一)
1最近使用python编写一个程序,遇到了导入包和参数引入的问题,后知后觉地发现c++和python关于变量存储和对象使用上的区别。
变量存储的机制上
众所周知,python没有c/c++里类似i++和++i这样语法。c/c++中,用户可以使用i++和++i进行整型变量的自增和数据结构的迭代器的自增。而i++和++i的区别在于,i++会返回一个自增前变量的拷贝副本,这个副本被存放在一个新的位置,而自增后的变量数值会被放回原来的地址,副本和变量本身无直接关系。
xxxxxxxxxx1// c++ 202int main(){3 int i = 0;4 cout << &i << endl; // 0x61fe085 int i2 = i++;6 cout << &i2 << endl; // 0x61fe047 cout << &i << endl; // 0x61fe088 return 0;9}与c++不同,python中,所有一切都被视为对象,且分为不可变对象和可变对象。
1.可变对象:大多数的容器类,包括列表、字典、集合和用户定义的对象。
2.不可变对象:包括整数、浮点数、字符串、元组和冻结集合。
可变对象很好理解,可以在对象元素的对应位置修改内容,而不具体改变对象所在地址,即不需要创建新的对象。实际上,python的列表对象就就是由一堆指向元素的指针和一些其他功能函数组成。需要注意的是,由于python对内存显式控制不敏感,python中的一个int对象所占空间会远大于在c中的int变量,其他的一些基本类型也是如此。以后可能要专门开一篇挖掘下。
不可变对象就比较麻烦,因为它本身不可被修改的特性,使得在对它进行任何“修改”操作时,进行得不是实际得修改,而是重新绑定,这与C++中的移动语义有些相似。这使得虽然看上去能对对象进行切片操作,但实际上不能修改。于是,你会在python见到如此场景。
xxxxxxxxxx61# python 3.10.62i1,i2 = 1,13if i1 == i2: print("i1 == i2") # i1 == i24if i1 is i2: print("i1 is i2") # i1 is i25print(id(i1)) #0x214468a00f06print(id(i2)) #0x214468a00f0
与c中每个变量分配一个地址不同,i1和i2是分别注册的整型变量,两者却地址相同。这就是python设计不可变对象的初衷之一,使用对象缓存优化对常量进行保存,包括小整数缓存(CPython中规定范围是-5至256,但实际会远超这个范围)、字符串驻留、函数默认参数缓存、方法解析缓存等等。因为它们常常会在程序中频繁被使用,使用该方法可以减小内存占用。值得注意的是,True、False、None(尺寸:28B、24B、16B)这些常用布尔值也是有被缓存的,在python程序以单例模式存在。
xxxxxxxxxx81# python 3.10.62i1,i2 = 1,13print(i1 is i2) # True4print(id(i1) == id(1)) # True5s1,s2 = "hello","hello"6print(s1 is s2) # True7# 相当于直接提供了一些较短字符串的右值引用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.62print(True is bool(1)) # True3print(False is bool(0))# True4print(None == False) # False5print(bool(None) == False) # True6print(0 == False,1 == True) # True True7print(100 == True,[1,2] == True) # False False设计不可变对象的还有一个好处是安全性。在普通的拷贝对象(值传递)时,原对象和拷贝对象可以“指向”同一个地址,减少内存占用。而在其中一个对象改变时,保留另一个对象的指向不变,创建一个新的对象存储,不需要担心其他绑定在不可变对象上的其他对象被修改。相比之下,C/C++中,每次拷贝数据结构或对象时,拷贝对象(数据结构)都会存储在一个新的地址,会占用更多的空间。不过python的这种转移是隐性的,有时会造成一些问题。例如自增时,对象地址会不断变化,后面需要判断一个变量是否是原来的那个变量;向函数传递变量时,如果传入的实参是不可变对象,那么在函数内对该变量的写入操作都只会作用在函数内创建的局部变量上。

python与c/c++在变量自增下存储区别
xxxxxxxxxx1# python 3.10.62i1 = 13i2 = i14print(i1,i2)5print(hex(id(i1)),hex(id(i2)))6i1+=17print(i1,i2)8print(hex(id(i1)),hex(id(i2)))9
10# 结果,不可变对象被拷贝后,只是增加一个对原地址上增加一个引用,c/c++同样可以用引用实现111 1120x216df2500f0 0x216df2500f0132 1140x216df250110 0x216df2500f0xxxxxxxxxx1// c++ 202class 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 1200x55845ff75c 0x55845ff758 212 1c++和python支持将字面量、自定义对象、全局对象作为占位的默认参数。其中,有一种“默认参数陷阱”也是值得注意的。python中,在默认参数未修改前,其指向全局变量空间/静态变量空间,故参数初始化的会被放在静态变量空间中,而不是栈空间。如果是不可变对象那很ok,但如果是可变对象,会导致对其的写入操作在函数退出后会被保存,这种隐式操作常常会导致预料外的结果,既不报错,也不停止。这种函数设计也是不符合python规范的。
xxxxxxxxxx1# python 3.10.62def 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= 116fuc id(arg1)= 0x2b14c8600f017fuc arg2= [1]18fuc id(arg2)= 0x2b14cc72a8019outside id(1)= 0x2b14c8600f0 #如果没有写入新数据,arg1指向静态变量区的120fuc arg1= 221fuc id(arg1)= 0x2b14c86011022fuc arg2= [1, 2]23fuc id(arg2)= 0x2b14cc72a8024fuc arg1= 325fuc id(arg1)= 0x2b14c86013026fuc 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 = data35 def getdata(self):36 return self._data37 def changedata(self,data):38 self._data = data39
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'''结果49105020 5120 # 没有重新申请对象523053'''C++中默认参数可以是NULL、nullptr、字面量、字符串、自定义对象(可见的变量)。但需要注意基本数组和字符串作为默认参数时,数组在作为参数时会退化成指针,无法将数组字面量传给指针,字符串则是不能进行修改。如果要使用数组和字符串作为默认参数,最好使用stl容器array、vector、string等或者自定义容器。
xxxxxxxxxx311// c++ 202void 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 03110 11 12 0 032
33// 先声明占位,在初始化在全局/静态位置也是ok的34void change(int to, int array[] = nullptr) { // 默认为 nullptr35 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的设计哲学更加重视“方法实现”,而不在意“对象类型”,不过留到下次再写吧。