python和c++面对对象部分对比
类的定义&声明
class
是定义或声明类的关键字- 类的名称一般首字母大写,用于和其他标识符区分
- c++中的类是结构体的升级(构造新的类型);而python中的类,类似于函数
c++ 类的声明(不占内存)
class Stu{
public:
char *name;
int age;
// 类内定义函数(默认内联函数),不占内存,但由于是替换展开,会增大代码量
void say(char* concent) {
printf("%s的年龄是%d, %s", name, age, concent);
}
void say_hello();
};
// 类外定义函数(域解析符::指明函数所属的类),占内存!类的对象共享类函数内存
void Stu::say_hello() {
printf("hello");
}
- 成员(变量或函数)均为数据声明,而不是定义,不占内存
函数的定义可以在类内或类外,类内定义默认为内联函数,建议在类外通过域解析符::
定义,类内只进行函数声明;多文件编程时,类的声明在头文件,类函数定义在源文件 - 成员权限默认为
private
(在类的外部,无法通过对象访问成员),使用public
关键字给予成员公开访问权限
详见 类成员的访问权限及类的封装 - 方法的函数体内可以直接调用属性
详见 this指针
python 类的定义(占内存)
class Stu:
age = 11 # 类变量
name = "Tom"
def say(self, s): # 实例变量
self.ok = "ok"
print("%s的年龄是%d, %s" % (self.name, self.age, s))
# 用type()内置函数定义类
def say(self, s): # 实例变量
self.ok = "ok"
print("%s的年龄是%d, %s" % (self.name, self.age, s))
Stu = type("Stu", (object,), dict(age=11, name='Tom', say = say))
demo = Stu()
demo.say('ok')
- 成员函数和类变量已完成初始化,占内存
age和name为类变量;ok为通过self.变量名
在成员函数内定义的实例变量(在类定义时不占内存,成员函数运行后才占内存);say()为实例函数
详见类属性和实例属性 - python没有
public
和private
关键字,不对访问权限进行限制和保护;成员权限默认为public
,只能通过命名规范在成员名前加双下划线__
,将其视为私有
详见 类成员的访问权限及类的封装 - 成员函数必须含self入参!无论函数体内是否需要通过
self
参数调用类属性
详见 self
对象创建 和 成员访问
-
对象内存模型
每个对象占的空间存储该对象的数据部分,不包含成员函数。类的所有成员函数都放在一个特殊的位置,类的对象共用成员函数 -
对象创建
对象创建时,完成了成员变量的定义(未初始化,值为随机数),没有进行成员函数定义
1)在栈上创建对象(有名字)类名 对象名;
Stu tom; Stu *p_tom = &tom; // 创建对象指针
对象删除:栈内存由程序自动管理,无法手动删除栈上创建的对象
2)在堆上创建对象(匿名),只能用对象指针表示
类名 *对象指针名 = new 类名; // 使用new关键字
Stu *tom = new Stu;
对象删除:堆内存由程序员管理,对象使用完毕后可手动删除,
delete 对象指针名;
;new和delete往往成对出现,防止无用内存堆积 -
成员访问
1)通过对象名字或引用(别名)访问成员:.
对象名.变量名 对象名.函数名(参数列表)
2)通过对象指针访问成员 :
->
对象指针->变量名 对象指针->函数名(参数列表)
python
- 对象创建
对象名 = 类名() # 括号不可省略!
tom = Stu()
- 成员访问
详见类属性和实例属性,一般用对象访问实例属性,用类访问类属性;类名.类变量名 对象名.实例变量名或类变量名 对象名.函数名(参数列表) # 参数列表为空时,函数调用的括号不可省略!
构造函数
- 构造函数是特殊的成员函数,在实例对象创建时会自动调用,常用于给 c++中的成员变量/python中的实例变量 进行初始化;
- 构造函数一旦添加就一定会在对象创建时自发调用,所以在创建对象时必须传入相应参数
- 每个类都存在默认构造函数(除self无其他入参),编辑器定义的函数体为空,用户可以自定义函数体,但如果自定义时改变了参数列表,默认构造函数就不再存在
c++
- 手动添加构造函数
构造函数名即为类名;// 类内声明 public: 类名(...); // 类外定义 类名::类名(...) { 代码块 }
构造函数必须为public属性,不然无法实现自动调用;
构造函数没有返回值:函数体没有返回语句,且定义时或声明时没有返回值类型,即使void也不允许 - 创建对象
创建对象时参数列表为构造函数入参;列表为空时,括号可省略类名 对象名(参数列表); // 栈上创建对象 类名 *对象指针名 = new 类名(参数;列表) // 堆上创建对象
例:
class Stu {
char *m_name; // 在成员变量名前加"m_"和成员函数入参区分
int m_age;
public:
Stu(char *name, int age);
void say();
};
Stu::Stu(char *name, int age) {
m_name = name;
m_age = age;
}
void Stu::say() {
printf("%s的年龄是%d\n", m_name, m_age);
}
void main() {
Stu tom("Tom", 11);
tom.say();
Stu *p_tom = new Stu("Tom", 11);
p_tom->say();
}
1)构造函数的重载
构造函数允许重载,实际上默认构造函数和手动添加的构造函数之间就会形成重载
class Stu {
char *m_name;
int m_age;
public:
Stu(char *name, int age); // 构造函数1
Stu(); // 构造函数2
void say();
};
Stu::Stu(char *name, int age) {
m_name = name;
m_age = age;
}
Stu::Stu() {
m_name = NULL;
m_age = 0;
}
void Stu::say() {
printf("%s的年龄是%d\n", m_name, m_age); // (null)的年龄是0
}
2)构造函数初始化列表
通过构造函数的初始化列表进行成员变量初始化
Stu::Stu(char *name, int age) {
m_name = name;
m_age = age;
}
简化为:
Stu::Stu(char *name, int age):m_name(name), m_age(age) {}
注意:
- 初始化列表使得代码更简洁,但是并没有效率上的优势,仅仅是为了书写方便
- 初始化列表中无法使用
this
指针,this
只有在方法内能使用 - 成员变量初始化顺序和初始化列表中的顺序无关,只和类中声明顺序有关
class Demo { int m_a; int m_b; public: Demo(int); void say(); }; Demo::Demo(int b):m_b(b), m_a(m_b){} void Demo::say() { cout << m_a << ", " << m_b << endl; } void main() { Demo demo(1); demo.say(); // -858993460, 1:并不是按初始化列表的顺序先给m_b赋值,而是按声明顺序先给m_a赋值(此时m_b的值为编译器定义时赋予的随机数) }
- const成员变量和引用类型成员,只能用初始化列表进行初始化,因为它们需要在定义的同时初始化
class Demo { const int m_a; public: Demo(int); }; Demo::Demo(int a) :m_a(a) {};
python
- 手动添加构造函数
方法名的开头和结尾各有两个下划线,且中间不能有空格;def __init__(self,...): 代码块
入参中self不可省略且必须作为第一个参数 - 创建对象
创建对象时的参数列表为构造函数除self以外的入参;参数列表为空时,括号依旧不可省略!对象名 = 类名(参数列表)
例:
class Stu:
def __init__(self, age, name): # 定义并初始化实例变量
self.__age = age # 在类函数内部,以“self.变量名”定义的变量为实例变量
self.__name = name
def say(self):
print("%s的年龄是%d" % (self.__name, self.__age))
tom = Stu(11, "Tom")
tom.say() # Tom的年龄是11
析构函数
析构函数时一种特殊的成员函数,销毁对象时(无论手动自动)系统会自动调用析构函数来进行清理工作,如释放分配的内存、关闭打开的文件…
c++
- 析构函数命名为
~类名
- 析构函数没有入参,不能被重载
// 模拟变长数组
class VLA{
int *p;
public:
VLA(int len);
~VLA();
};
VLA::VLA(int len){
p = new int[len]; // 对象销毁时成员变量p被销毁,但分配在堆区的内存不会被释放,需要析构函数中进行手动释放
}
VLA::~VLA() {
delete[] p;
cout << "清理完毕" << endl;
}
int main(){
VLA demo(4); // main()结束后自动销毁demo对象并执行析构函数
}
对象销毁(析构函数执行)时机:
-
通过
new
创建的对象在堆区,在无用后需要使用delete
关键字手动销毁,不手动销毁无用对象的话即使程序结束也不一定会销毁对象,内存一直不释放存在内存泄露问题建议new之后进行delete,防止内存泄露
-
自动销毁的时机则和对象的作用域有关:全局数据区内的全局对象在程序结束时销毁并调用析构函数;栈区内的局部对象在函数结束时销毁调用析构函数
class Demo{
char *m_str;
public:
Demo(char *str);
~Demo();
};
Demo::Demo(char *str):m_str(str){}
Demo::~Demo() { cout << m_str << endl; }
Demo obj1("全局数据区"); // 程序结束时自动销毁obj1,调用析构函数
int main(){
Demo obj2("栈区"); // main函数结束时自动销毁obj2,调用析构函数
new Demo("堆区"); // 不使用delete则不会被销毁
}
输出
栈区
全局数据区
python
- 析构函数名为
__del__
- 析构函数唯一的入参是self
class Demo:
def __init__(self, f_path):
self.f = open(f_path, 'w')
def __del__(self):
self.f.close()
d = Demo('1.txt')
d.f.write('hello')
del d // 手动删除对象,调用析构函数
python采用自动引用计数实现垃圾回收机制:
对象初始计数为0,在被变量引用时(赋值/传参)计数加1,引用取消时计数减1。当一个对象计数为0时意味着没有变量引用该对象,python就会自动销毁对象并调用析构函数
对象销毁的时机:
- 程序退出
- 对象计数归0
- gc.collect()
class Demo:
def __init__(self):
print("初始化")
def __del__(self):
print("对象销毁:清理内存")
d1 = Demo()
d2 = d1
del d1
析构函数没有因为del执行,因为del手动销毁的实际是对象的引用(变量名)而不是对象数据本身,对象计数未归0
类成员的访问权限及类的封装
- 访问权限:类成员在类体内访问不受限制,但在类外,访问权限存在区别
- 封装:隐藏类的内部实现(类属性和部分方法),只向用户通提供部分方法间接访问类属性
c++
1. 访问权限
通过public、protected、private三个成员访问限定符控制成员函数和变量的访问权限;
三种属性的成员在类体内部访问不受限制;但在类外,private和public属性成员无法通过对象访问,只有pulic可以
class Stu{
char *name; // 类内成员默认为private属性
public:
int age;
void say(char *content);
};
void Stu::say(char *content) {
printf("%s的年龄是%d, %s", name, age, content);
} // 成员访问成功:类体内的成员访问不受限,::符连接了函数与类
void main() {
Stu Tom;
Tom.name = "Tom"; // 成员访问失败:private属性成员无法在类外通过对象访问
}
proteced属性详见 继承
2. 封装
在实际项目中,成员变量均声明为private(建议以m_
开头,和成员函数的形参进行区分),只将部分允许通过对象访问的成员函数声明为public
在类外修改private成员变量的值:
- 通过添加构造函数,为成员变量赋初值
- 设置两个public属性的函数成员,一个为set函数(设置成员变量值),一个get函数(读取成员变量值)
class Stu{ char *m_name; // 隐藏成员变量 public: // 提供需要的成员函数(接口) Stu(char *name); void setname(char *name); char *getname(); }; Stu::Stu(char *name) { m_name = name; } void Stu::setname(char *name) { m_name = name; } char *Stu::getname() { return m_name; } void main() { Stu tom("default"); tom.setname("Tom"); cout << tom.getname() << endl; }
python
1. 访问权限
和c++不同,python类中的函数和变量只有private和public两种属性
python没有提供访问权限修饰符,而是通过命名“投机取巧”地区分private和public:
- 默认情况下,python类中的成员都是public属性,名称前没有双下划线
- 当类中的成员以双下划线
__
开头,成员可视为private属性(也方便和成员函数的形参进行区分)
实际上,双下划线开头的成员在编译后会被重命名为_类名__成员名
,所以类外无法通过对象.成员名
直接调用class Stu: def __init__(self, age, name): self.__age = age self.__name = name def __say(self): # 在底层被重命名为_Stu__say print("%s的年龄是%d" % (self.__name, self.__age)) tom = Stu(11, "Tom") tom._Stu__say() # Tom的年龄是11
2. 封装
实际项目中,成员变量都在名字前加双下划线,设置为private属性,只留部分类外需要使用的方法默认为public属性
在类外修改private成员变量的值:
- 添加__init__构造函数,为成员变量(基本是实例变量)赋初值
- 添加set、get函数,查看或修改成员变量的值
class Stu: def __init__(self, name): self.__name = name def setname(self, name): self.__name = name def getname(self): return self.__name tom = Stu("") tom.setname("tom") tom.getname()
- 通过property函数或 @property装饰器,在不不破坏封装的前提下,将调用set、get…成员函数操作类属性,简洁成常用的
对象.属性
格式
property函数(实际是property类):
@property装饰器:class Stu: def __init__(self, name): self.__name = name def setname(self, name): self.__name = name def getname(self): return self.__name def delname(self): self.__name = "xxx" name = property(getname, setname, delname) # property函数入参可以是1-4个:fget、fset、fdel、doc,实际是创建property类的对象并初始化 tom = Stu("") tom.name = "Tom" # 实际是调用property对象的__setattr__方法(内含setname的调用) print(tom.name) # 相当于执行getname() del tom.name # 相当于执行delname() print(tom.name)
class Stu: def __init__(self, name): self.__name = "xxx" @property # 修饰下面的方法name,使之成为name属性的getter方法 def name(self): return self.__name @name.setter def name(self, name): self.__name = name @name.deleter def name(self): self.__name = "xxx" tom = Stu("") tom.name = "Tom" print(tom.name) del tom.name tom.name
this指针和self
c++:this指针
const类型的this指针实际是成员函数隐式形参,在对象在调用成员函数时,编译器会自动将对象的地址作为实参传递给this指针,这样通过this指针和->
符就可以访问对象内存中的成员变量
(结合对象内存模型理解,this指针实际是对象和成员函数关联的桥梁)
- this只能在成员函数内部使用(因为它是函数形参)
- this是const指针,其值一旦在给成员函数传参时完成初始化,就不能在函数内再被修改
- 只有对象被创建后this才有意义,所有不能在static成员函数中使用
class Stu{
char *name;
public:
Stu(char *name);
char *getname();
};
Stu::Stu(char *name) {
this->name = name; // c++成员函数隐性地使用this作为入参后,函数内可直接访问成员变量,但如果成员变量和成员函数形参重名,只好使用this指针调用
}
char *Stu::getname() {
return this->name;
}
void main() {
Stu tom("xxx");
cout << tom.getname() << endl;
}
python:self
self参数类似于this指针,当对象调用类方法时,编译器会自动把对象自身的引用作为方法的第一个参数,该参数实际没有具体命名规定,命名为self只是约定俗成的习惯
class Stu:
name = "xxx"
def __init__(self, name):
self.name = name # python实例方法使用self作为入参后,方法内需要使用"self."结构调用方法或属性省略
tom = Stu("tom")
tom.name
c++
类默认成员函数
8个类默认函数:默认构造函数、默认copy构造函数、默认move构造函数(c++ 11)、默认析构函数、默认重载=运算符函数、默认重载&运算符函数、默认重载&运算符const函数、默认重载移动赋值操作符函数(C++11)
1 class A
2 {
3 public:
4
5 // 默认构造函数;
6 A();
7
8 // 默认拷贝构造函数
9 A(const A&);
10
11 // 默认析构函数
12 ~A();
13
14 // 默认重载赋值运算符函数
15 A& operator = (const A&);
16
17 // 默认重载取址运算符函数
18 A* operator & ();
19
20 // 默认重载取址运算符const函数
21 const A* operator & () const;
22
23 // 默认移动构造函数
24 A(A&&);
25
26 // 默认重载移动赋值操作符
27 A& operator = (const A&&);
28
29 };
静态成员变量、成员函数
静态成员可以用对象访问,也可以用类访问;普通成员只能用对象访问
1)静态成员变量
其特质类似于成员函数,但成员函数无法通过类访问,static成员变量可以
- 在类声明内用
static
关键字修饰的成员变量 - 静态成员变量不在类声明时完成定义,也不在对象创建时完成定义,而是必须在类声明外单独进行定义
在全局数据区分配内存,不随对象的创建分配内存,也不随对象的销毁释放内存,而是在程序结束时才释放;
定义时可以不初始化,默认0(全局数据区内变量初始值都为0,动态数据区变量默认值一般是垃圾值) - static成员变量属于类,类的对象共享static成员变量(有读写权限),也就是说只为satic成员变量分配一份内存,通过对象可以改变静态成员变量
class Demo{
public:
static int num_total;
Demo();
void show();
};
int Demo::num_total = 0;
Demo::Demo(){
num_total++;
}
void Demo::show(){
cout << num_total << endl;
}
int main(){
(new Demo)->show();
(new Demo)->show();
}
- 在对象创建之前就可以通过类访问静态成员变量,也可以通过对象访问,但依旧受public、protected、private访问权限限制
cout << Demo::num_total << endl;
Demo *p = new Demo;
cout << p->num_total << endl;
2)静态成员函数
和普通成员函数的区别:
- 静态成员函数没有this指针,也就无法访问普通成员变量(在对象内存空间中),只能访问静态成员变量(属于类)
- 静态成员函数可以通过类访问(一般都这么做,是静态成员函数的优势所在),也可以通过对象访问;而普通成员变量只能通过对象访问
class Demo{
public:
static int num_total;
Demo();
static void show();
};
int Demo::num_total = 0;
Demo::Demo(){
num_total++;
}
void Demo::show(){
cout << num_total << endl;
}
int main(){
new Demo;
Demo::show();
}
const成员变量、成员函数、对象
-
const成员变量
不能修改的成员变量,只能通过构造函数初始化列表进行初始化(详见构造函数初始化列表) -
const成员函数
对类中所有成员变量可读不可写(const成员函数返回值若为成员变量的引用,通过返回值修改成员变量也会报错),目的是保护数据,常用于get函数必须在成员函数的声明和定义处同时加上const关键字
class Stu{ char *m_name; public: Stu(char *name); char *getname() const; }; Stu::Stu(char *name):m_name(name){} char *Stu::getname() const{ return m_name; } int main(){ cout << (new Stu("tom"))->getname() << endl; }
-
const对象
只能调用类的const成员(常成员变量和常成员函数),保证不修改对象的数据
定义方法:const 类名 object(params); const 类名 *object_p = new 类名(params);
class Stu{ char *m_name; public: Stu(char *name); char *getname() const; }; Stu::Stu(char *name):m_name(name){} char *Stu::getname()const{ return m_name; } int main(){ const Stu tom("Tom"); cout << tom.getname() << endl; }
友元函数和友元类(friend关键字)
友元关系是单向且不可传递的;在不破坏封装的前提下提高了灵活性(如设定某私有变量可以通过某个类修改)
-
友元函数
借助友元(friend
)可以使其他类中的成员函数以及不属于任何类的函数有通过该类的对象访问对象private成员的权限
1)将非成员函数声明为友元函数class Stu{ char *m_name; public: Stu(char *name); friend void show(Stu *); }; Stu::Stu(char *name):m_name(name){} void show(Stu *student){ cout << student->m_name<< endl; } int main(){ show(new Stu("Tom")); }
类的成员函数由于入参有this指针,可以直接调用对象的成员变量,而非成员函数想要访问类的成员变量就必须借助对象,入参可以是对象指针也可以是引用
2)将其他类的成员函数声明为友元函数
class Address; // 提前声明Address类,因为下面Stu类使用了它 class Stu{ char *m_name; public: Stu(char *name); void showAddress(Address *); }; class Address{ char *m_city; public: Address(char *); friend void Stu::showAddress(Address *); }; Stu::Stu(char *name):m_name(name){} void Stu::showAddress(Address *addr){ cout << addr->m_city << endl; } // Stu成员函数的定义放在Address类声明后,因为用到了Address里中成员变量 Address::Address(char *city):m_city(city){} int main(){ (new Stu("tom"))->showAddress(new Address("shaoxin")); }
-
友元类
在类A内将类B声明为友元,则类B中的所有成员函数都是类A的友元函数,可以通过类A的对象访问对象private成员class Address; class Stu{ char *m_name; public: Stu(char *name); void showAddress(Address *); }; class Address{ char *m_city; public: Address(char *); friend class Stu; }; Stu::Stu(char *name):m_name(name){} void Stu::showAddress(Address *addr){ cout << addr->m_city << endl; } Address::Address(char *city):m_city(city){} int main(){ (new Stu("tom"))->showAddress(new Address("shaoxin")); }
一般不建议将整个类声明为友元类,不安全,将某些成员函数声明为友元函数就够了
python
成员属性:类属性、实例属性
python成员属性根据定义位置和方式的不同可以分为两种:类属性和实例属性
1)类属性
类属性是在类体内且在所有类方法外定义的成员变量
-
一旦类定义完成,类属性也完成了定义;创建的对象共享类中定义的类属性(只读不可写)
-
可以通过类访问和修改类属性;通过对象只能访问(不建议)无法修改类属性(因为类属性是所有该类的对象共享的,通过对象修改的只能是新添加的同名实例属性)
class Stu: name = "xxx" print(Stu.name) # xxx:通过类访问类属性 tom = Stu() print(tom.name) # xxx:通过对象访问类属性 Stu.name = "Tom" # 通过类修改类属性 print(tom.name) # Tom:类的对象共享类中定义的类属性,所以通过类修改类属性后,对象的类属性也随之改变 tom.name = "xxx" # 通过对象无法修改类属性,只会创建新的同名实例属性 print(Stu.name) # Tom
类属性和实例实例属性同名时,通过实例对象优先访问的是实例属性:
class Stu: name = "类属性" def __init__(self, name): self.name = name print(Stu.name) # "类属性" tom = Stu("实例属性") print(tom.name) # "实例属性"
-
通过类可以动态添加、删除类属性;无法通过对象动态添加或删除类属性
class Stu: name = "xxx" del Stu.name # 删除类中本来就定义好的类属性 Stu.age = "11" del Stu.age # 删除动态添加的类属性
2)实例属性
实例属性是在类方法内以self.变量名
的方式定义的成员变量
- 类和对象定义完成,并调用实例属性所在方法后,实例属性才完成定义
- 只能通过对象无法通过类访问或修改实例属性,因为类的定义里根本没有完成实例属性创建
class Stu:
def setname(self, name):
self.name = name
tom = Stu()
# tom.name 报错,因为实例属性所在方法被对象调用后,实例属性才创建完成
tom.setname("tom")
print(tom.name) # 通过实例访问实例属性
tom.name = "xxx" # 通过实例修改实例属性
print(tom.name)
- 通过对象可以动态添加或删除实例属性
class Stu: def __init__(self, name): self.name = name tom = Stu("Tom") tom.age = 11 # Stu.age 报错:添加的是实例属性,所以通过类无法访问 del tom.age del tom.name
成员方法:实例方法、类方法、静态方法
在类体内成员方法定义完成后,对象共享类体内的成员方法,类似于c++的对象内存模型
-
定义上的区别(参数不同)
实例方法:在类中定义的方法默认都是;最少要包含一个参数,约定俗成命名为self
,其绑定的是对象的引用类方法:通过装饰器@calssmethod进行修饰;和实例方法类似,也最少要包含一个参数,约定俗成命名为
cls
,其绑定的是类的引用静态方法:通过装饰器@staticmethod进行修饰;没有和类或对象的引用绑定,和普通函数唯一的区别就是定义在类内
-
功能间的区别
实例方法:可以获取类属性也可以获取实例属性;可以获取其他成员函数类方法:只能获取类属性无法获取实例属性;可以获取其他成员函数
静态方法:无法获取类属性也无法获取实例属性;无法获取其他成员函数
-
调用格式的区别
实例方法:建议用实例调用,但也可以通过类调用(不建议,需要手动把对象传给self)类方法:建议用类调用,但也可以通过实例调用(不建议)
静态方法:可以使用类名调用,也可以使用实例调用
例:
class Stu:
age = 0
def __init__(self):
self.name = "xxx"
def demo(self):
self.name, self.age
self.demo, self.demo_cls, self.demo_sta
print("正在调用实例方法")
@classmethod
def demo_cls(cls):
cls.age # 无法调用实例属性:因为cls绑定的是类,类的定义里没有实例属性
cls.demo, cls.demo_cls, cls.demo_sta
print("正在调用类方法")
@staticmethod
def demo_sta():
print("正在调用静态方法")
student = Stu()
student.demo()
Stu.demo(student) # 用类调用实例方法必须传入对象给self
Stu.demo_cls()
student.demo_cls()
Stu.demo_sta()
student.demo_sta()
- 动态添加/删除方法
通过类和对象都可以动态添加和删除方法,区别在于用类添加的方法可以被类的对象共享,而用对象添加的方法只属于该对象
只有通过对象新增的实例方法可以通过对象删除,类中定义的实例方法无法删除(因为是共享的):class Stu: age = 0 def demo(self): pass def demo1(self): print("实例方法已添加") student = Stu() student.demo1 = demo1 # Stu.demo1 报错:因为通过实例动态增加的实例方法只属于该实例而不是共享的
新增的方法在调用时,无法自动将调用该方法的对象或类绑定到self,需要手动绑定:del student.demo # 失败 del student.demo1 # 成功
使用types 模块下的 MethodType添加方法,在方法调用时可实现自动绑定;student.demo1 = demo1 student.demo1(student)
from types import MethodType student.demo1 = MethodType(demo1, student) student.demo1()
继承
继承是子类从父类获取成员变量和成员函数的过程
c++
class 子类名: [继承方式] 父类名{
子类新增加的成员声明
};
继承的三种方式
继承方式用于指明父类成员在子类中的最高访问权限,包括public、private(默认的继承方式)和protected三种,权限由高到低public->protected->private
1)public继承方式
- 父类中的public成员在派生类中为public属性
- 父类中的protected成员在派生类中为protected属性
- 父类中的private成员在派生类中不能使用
2)protected继承方式
- 父类中的public成员在派生类中为protected属性
- 父类中的protected成员在派生类中为protected属性
- 父类中的private成员在派生类中不能使用
3)private继承方式
- 父类中的public成员在派生类中为private属性
- 父类中的protected成员在派生类中为private属性
- 父类中的private成员在派生类中不能使用
规律总结:
- 父类成员在子类中的访问权限只会由于继承方式的限制而降低,不会升高
- protected和private属性在父类中都不能通过对象访问,区别在于:protected属性在被子类继承后是可见的;而private属性被继承后虽然也会占子类或子类对象内存,却不能使用(不管用什么继承方式)
- 子类中访问父类private成员的唯一方式就是借助父类非private成员函数
- 实际开发一般用public继承方式,因为private和protected继承方式会改变父类成员在子类中的访问权限,导致继承关系复杂
例:
class People{
public:
void setname(char *);
void sethobby(char *);
char *gethobby();
protected:
char *m_name;
private:
char *m_hobby; // 子类只能通过gethobby()访问m_hobby
};
void People::setname(char *name) { m_name = name; }
void People::sethobby(char *hobby) { m_hobby = hobby; }
char *People::gethobby() { return m_hobby; }
class Student: public People {
private:
float m_score;
public:
void setscore(float);
void display();
};
void Student::setscore(float score) { m_score = score; }
void Student::display() { cout << "name:" << m_name << ", score:" << m_score << ", hobby:" << gethobby() << endl; }
int main(){
Student tom;
tom.sethobby("love sport");
tom.setname("tom");
tom.setscore(88);
tom.display();
}
使用using关键字可以改变protected和private父类成员在子类中的访问权限(无法改变private成员的访问权限,因为父类private成员在子类中不可见):
class Student: public People {
private:
float m_score;
using People::gethobby; // 将gethobby在子类中的访问权限由public改为private
public:
void setscore(float);
void display();
};
继承时的作用域嵌套和名字遮蔽
参考继承时的对象内存模型,派生类作用域实际位于基类作用域内,成员的查询遵循“从小的作用域向大的作用域查找”,一旦根据成员名在派生类中查询到了成员,就不会再向父类查找,所以派生类中新增的成员会遮蔽从基类继承的同名成员,根据这一特性实现可函数重写功能
class People{
public:
void show();
};
void People::show(){
cout << "people" << endl;
}
class Student: public People {
public:
void show();
};
void Student::show(){
cout << "student" << endl;
}
int main(){
Student stu;
stu.show(); // student
}
如果要访问被遮蔽的同名基类成员,需要加上类名和域解析符stu.People::show();
基类成员函数和从派生类继承成员函数不能构成重载,因为作用域不同,一旦重名就只会造成遮蔽,编译器根据函数名定位到派生类中方法后,不管参数列表符不符合,都不会再搜索基类作用域
基类和派生类的构造函数
类的构造函数不可以被继承,因为即使继承了,由于名字和子类不同,不会在对象创建时被自动调用
从父类继承的private变量在子类不可见也就无法直接使用子类构造函数初始化;由于没有继承父类构造函数也无法在子类构造函数中调用它初始化继承于父类的private变量。解决方法:在子类构造函数初始化列表中调用基类的构造函数
class People{
private:
char *m_name;
public:
People(char *);
char *getname();
};
People::People(char *name):m_name(name){}
char *People::getname() { return m_name; }
class Student: public People {
private:
float m_score;
public:
Student(char *, float);
void display();
};
Student::Student(char *name, float score): m_score(score), People(name) {}
void Student::display(){
cout << "name:" << getname() << ", score:" << m_score;
}
int main(){
Student tom("Tom", 91);
tom.display();
}
- 子类构造函数初始化列表中父类构造函数可以放前面也可以放后面,但在执行时,必然先执行父类构造函数
- 事实上,语法规定了派生类的构造函数初始化列表必须调用父类构造函数。如果没有指明调用对象,编译器就会调用基类默认构造函数
class People{ private: char *m_name; public: People(char *); People(); # 基类默认构造函数。如果去掉该行,People就没有默认构造函数,只有自定义的 char *getname(); }; People::People(char *name):m_name(name){} People::People():m_name("xxx"){} char *People::getname() { return m_name; } class Student: public People { private: float m_score; public: Student(float); void display(); }; Student::Student(float score): m_score(score){} # 没有指定父类构造函数时,自动调用父类默认构造函数,如果父类默认构造函数不存在就会报错 void Student::display(){ cout << "name:" << getname() << ", score:" << m_score; } int main(){ Student tom(99); tom.display(); }
- 子类构造函数在初始化列表中只能调用直接父类的构造函数,不能调用间接父类的。不然会造成重复初始化,因为子类构造函数会自动调用其直接父类的默认构造函数
基类和派生类的析构函数
析构函数和构造函数一样不能被继承,但是子类析构函数初始化列表中无需显式调用父类析构函数,编译器在销毁对象执行子类析构函数时会自动调用父类默认的析构函数(析构函数参数列表只能为空)
析构函数执行顺序和构造函数相反:销毁子类对象时,先执行子类析构函数,再执行父类析构函数
class Parent{
public:
Parent();
~Parent();
};
Parent::Parent() { cout << "parent constructor" << endl; }
Parent::~Parent() { cout << "parenr destructor" << endl; }
class Child: public Parent {
public:
Child();
~Child();
};
Child::Child() { cout << "child constructor" << endl; }
Child::~Child() { cout << "child destructor" << endl; }
int main(){
Child test;
}
输出:
parent constructor
child constructor
child destructor
parent destructor
多继承
一个派生类可以有多个基类
class 子类名: 继承方式 父类名, ..., 继承方式 父类名 {
子类新增成员
}
-
多继承下的构造函数和析构函数
类似于单继承,只是多继承需要在子类构造函数初始化列表中调用多个基类的构造函数;子类名(形参列表):父类名(实参列表), ..., 父类名(实参列表)
不指定基类构造函数时,自动调用基类默认构造函数;
基类构造函数调用顺序与初始化列表中的顺序无关,只取决于派生类声明时基类出现的顺序;
析构函数执行顺序和构造函数相反class Parent1{ public: Parent1(); ~Parent1(); }; Parent1::Parent1() { cout << "parent1 constructor" << endl; } Parent1::~Parent1() { cout << "parent1 destructor" << endl; } class Parent2{ public: Parent2(); ~Parent2(); }; Parent2::Parent2() { cout << "parent2 constructor" << endl; } Parent2::~Parent2() { cout << "parent2 destructor" << endl; } class Child: public Parent1, public Parent2 { public: Child(); ~Child(); }; Child::Child() { cout << "child constructor" << endl; } Child::~Child() { cout << "child destructor" << endl; } int main(){ Child demo; }
-
多继承时除了子类对父类成员的屏蔽,还存在父类间的命名冲突
多继承多个父类中有成员同名时,直接访问该成员会产生命名冲突,需要在成员名前加类名和域解析符,消除二义性;父类之间不存在函数重载,因为作用域不同class Parent1{ public: void show(); }; void Parent1::show() { cout << "parent1" << endl; } class Parent2{ public: void show(); }; void Parent2::show() { cout << "parent2" << endl; } class Child: public Parent1, public Parent2 {}; int main(){ Child demo; demo.Parent1::show(); }
-
多继承容易让代码逻辑复杂、思路混乱,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。
虚继承
为了解决多继承时的命名冲突问题,c++引入虚继承和虚基类:
无论虚基类A在继承体系中出现了多少次,派生类中都只包含一份虚基类的成员,在访问从A中继承的成员时就不会同时出现A->B->D和A->C->D两条继承路径的二义性
class A{
protected:
int m_a;
};
class B: virtual public A{};
class C: virtual public A{};
class D: public B, public C{
public:
void seta(int);
};
void D::seta(int a) { m_a = a;} // m_a不存在二义性
int main(){
D d;
d.seta(1);
}
- 虚继承构造函数
和普通的继承(不能调用间接基类的构造函数)不同,在最终派生类D的构造函数需要调用虚基类A(间接基类)的构造函数进行初始化,因为调用直接父类B、C构造函数时,B和C对虚基类A构造函数的调用是无效的(为了防止B和C在调用A的构造函数时给出不同实参导致编译器不知道用哪个)class A{ private: int m_a; public: A(int); int geta(); }; A::A(int a): m_a(a){} int A::geta() { return m_a; } class B: virtual public A{ public: B(int); }; B::B(int a):A(a){} class C: virtual public A{ public: C(int); }; C::C(int a):A(a){} class D: public B, public C{ public: D(int); }; D::D(int a):A(a), B(2), C(3){} // 必然先调用虚基类A的构造函数,与初始化列表中的顺序无关;B()和C()对m_a的修改无效 int main(){ D d(1); cout << d.geta() << endl; // 1 }
继承时的对象内存模型
没有继承时对象内存模型:对象内存只包含成员变量,存储在栈区或堆区;成员函数单独存储在代码区
有继承关系时,派生类对象内存可视为基类成员变量和新增成员变量总和,成员函数仍存储在代码区由类的对象共享:
-
单继承
子类作用域在基类内:
子类m_a屏蔽父类m_a -
多继承
子类作用域在基类内,子类m_a屏蔽父类m_a;
多个父类作用域相互独立,所以直接调用时,父类A和B中m_b命名冲突,需要指明所在的域 -
虚继承
和单继承和多继承相反,虚继承时不管是虚基类的直接派生类还是间接派生类,继承于虚基类的成员变量始终放在新增成员变量后面而不是前面
B虚继承A,C单继承B:
向上转型
向上转型是安全的,可以由编译器自动完成,如float转int,类其实也是一种数据类型,存在数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只允许向上转型(派生类赋给基类)
-
将派生类对象赋值给基类对象
class A{ public: int m_a; A(int); void display(); }; A::A(int a): m_a(a){} void A::display(){ cout << m_a << endl; } class B: virtual public A{ public: int m_b; B(int, int); void display(); }; B::B(int a, int b):A(a), m_b(b){} void B::display(){ cout << m_a << m_b << endl; } int main(){ A a(1); B b(2, 3); a = b; a.display(); // 2:b向a赋值后,a的类型依旧是A,所以只能调用A的成员函数 a.m_b; // 编译失败:m_b在向上转型时被丢弃 b = a; // 编译失败:向下转型 }
赋值的本质是向内存填充数据,对象的内存只包含成员变量,所以对象间的赋值是成员变量的copy,不涉及成员函数(和类绑定);
派生类对象向基类对象赋值时,只填充了继承于基类的成员变量,超出基类对象内存的数据(派生类新增的成员变量和多继承时其他基类的成员变量)会被丢弃;基类向派生类赋值时,基类数据太少不足以填充派生类内存,编译器不知道怎么填充就会报错
-
派生类对象指针赋值给基类对象指针
class A{ public: int m_a; A(int); void display(); }; A::A(int a): m_a(a){} void A::display(){ cout << m_a << endl; } class B{ public: int m_b; B(int); void display(); }; B::B(int b):m_b(b){} void B::display(){ cout << m_b << endl; } class C: public A, public B{ public: C(int, int); }; C::C(int a, int b):A(a), B(b){} int main(){ A *pa = new A(1); B *pb = new B(2); C *pc = new C(3, 4); pa = pc; pb = pc; pa->display(); //3 cout << pa << endl; //0x2532810 cout << pb << endl; //0x2532814:和pc值不同 cout << pc << endl; //0x2532810 }
赋值后,基类指针指向派生类对象内存,就可以使用派生类成员变量数据;但基类指针的类型依旧是基类,所以只能使用基类成员函数(成员函数和类绑定,与this指针及对象内存无关)
指针之间赋值时,为了类型匹配指针类型,编译器会自动改变指针的值(就像子类对象赋值给基类时丢弃新增成员变量一样):当
C *
类型指针pc
向B *
类型指针pb
赋值时,编译器自动会偏移pb
的值,使其指向C类对象内存中继承于B类的成员变量首地址然后再赋值给pc
-
将派生类/派生类引用赋值给基类引用
int main(){ C pc(3, 4); A &pa = pc; pa.display(); //3 }
特性和指针一样,因为引用的本质就是对指针的封装
python
单继承和多继承
子类拥有父类所有属性和方法,即使该属性和方法通过命名进行了私有封装
-
单继承
class Person: __age = 0 # 已被封装为私有 hight = 0 def say(self): print('people:', self.name) class Student(Person): h = Person.hight/10 # 子类类体中需要使用父类类名调用父类属性和方法 print(Student._Person__age) # 0 tom = Student() tom.name = "Tom" tom.say() # student: Tom print(tom.h) # 0
-
多继承
class 类名(父类1, 父类2, ...): #类体
虽然 Python 在语法上支持多继承,但不建议使用!会使代码逻辑混乱
继承中的同名遮蔽问题:原理见类的MRO列表
- 子类新增成员屏蔽继承自父类的同名成员,通过这一特性可实现方法重写
class Person: def say(self): print('people:', self.name) class Student(Person): def say(self): # 遮蔽同名父类方法,实现重写 print('student:', self.name) tom = Student() tom.name = "Tom" # 先在Student里找name,找不到再到Person里找 tom.say() # student: Tom
- 按照继承顺序,排在前面的父类中的成员会屏蔽排在后面的父类中的同名成员
class Person: def say(self): print('people:', self.name) class Animal: def say(self): print('animal:', self.name) class Student(Person, Animal): pass tom = Student() tom.name = "Tom" tom.say() # people: Tom
如果要在类外调用被屏蔽的属性和方法,可以使用父类类名进行调用(调用方法时需手动给self传参)
Person.say(tom) # people: Tom
子类中,python如何找到成员
python会计算每个类的MRO列表(是包含该类本身及其继承链上所有基类的线性顺序列,如class C(A, B)
C的MRO列表中类的顺序为CAB,类对成员进行调用时,实际是在MRO列表中从左到右查找各个类,直到搜索到匹配的成员
所以子类成员屏蔽父类同名成员,先继承的父类成员又屏蔽后被继承的父类成员
子类和父类中的构造函数
和c++不同,python中父类的构造函数也会被子类继承,但被子类同名构造函数屏蔽,需要通过父类名调用
class Person:
def __init__(self, name):
self.name = name
class Animal:
def __init__(self, categoty):
self.category = categoty
class Student(Person, Animal):
def __init__(self, name, categoty):
Person.__init__(self, name) # 父类构造函数被同名子类构造函数屏蔽,需要用类名调用
Animal.__init__(self, categoty)
def display(self):
print("name:",self.name,', category:', self.category)
tom = Student('Tom', 'primate')
tom.display() # name: Tom , category: primate
可以使用super()函数简化父类构造函数的调用,super()生成当前类的MRO列表(代表类继承顺序),并返回当前类的后一个类
class Person:
def __init__(self, name):
self.name = name
class Animal:
def __init__(self, categoty):
self.category = categoty
class Student(Person, Animal):
def __init__(self, name, categoty):
super().__init__(name) # 无需传入self
Animal.__init__(self, categoty) # 无法用super()简化,因为super()只能返回Student在MRO列表中后一个类即Person,无法返回Animal
def display(self):
print("name:",self.name,', category:', self.category)
tom = Student('Tom', 'primate')
tom.display() # name: Tom , category: primate
多态
指同一名字的事物可以完成不同的功能
- c++中多态分为编译时的多态(函数重载包括运算符重载,模板函数)和运行时的多态(通过基类指针访问所有派生类的成员,涉及继承和虚函数)
- python中的多态就单指运行时的多态(子类对父类函数重写)
c++
多态与虚函数
向上转型时,基类指针指向了派生类对象,按照正常思维习惯,它应该可以调用派生类的成员变量和成员函数,但实际只能调用成员变量
class Language{
public:
void say();
};
void Language::say() { cout << "language" << endl; }
class Python: public Language{
public:
void say();
};
void Python::say() { cout << "python" << endl; }
int main(){
Language *demo = new Language;
demo->say(); //language
demo = new Python;
demo->say(); //language
}
为了使基类指针可以访问派生类成员函数,c++增加了虚函数:
class Language{
public:
virtual void say();
};
void Language::say() { cout << "language" << endl; }
class Python: public Language{
public:
virtual void say();
};
void Python::say() { cout << "python" << endl; }
int main(){
Language *p = new Language;
p->say(); //language
p = new Python;
p->say(); //python
}
虚函数和普通成员函数一样,不存在对象内存中,区别在于虚函数列表首地址会放在对象内存中(详见虚函数表),所以基类指针可以像访问派生类成员变量一样访问派生类虚函数,同一语句p->say()
在指针指向不同时可以有不同功能
c++中的多态指的就是通过基类指针可以访问所有派生类的成员变量和成员函数(虚函数)(不建议通过“引用”,因为c++中的引用只能指代固定对象,缺乏灵活性)
多态条件:
- 存在继承关系
- 子类对父类同名成员变量或函数原型相同的虚函数形成覆盖
- 使用基类指针调用基类及派生类成员
虚函数注意事项
-
如果成员函数在类的继承后希望通过同名遮蔽更改其功能,一般将其声明为虚函数,便于之后使用基类指针形成多态以简化编程
-
可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数
class Language{ public: virtual void say(); }; void Language::say() { cout << "language" << endl; } class Python: public Language{ public: void say(); // 自动成为虚函数 }; void Python::say() { cout << "python" << endl; } int main(){ Language *demo = new Language; demo->say(); //language demo = new Python; demo->say(); //python }
-
构造函数不能是虚函数,因为子类构造函数实际不会继承给父类,而仅仅是在可以在父类构造函数中被调用
-
只有派生类的虚函数覆盖(同名)基类的虚函数并且函数原型相同(参数列表也相同)才能构成多态
例如基类虚函数的原型为virtual void func();
,派生类虚函数的原型为virtual void func(int);
,那么当基类指针 p 指向派生类对象时,语句p -> func(100);
将会出错,而语句p -> func();
将调用基类的函数 -
析构函数可以声明为虚函数而且有时必须声明为虚函数
在堆上new派生类对象赋给基类指针后需要delete指针释放内存,如果析构函数不声明为虚函数,那么编辑器根据指针类型访问析构函数(成员函数和类绑定)class Parent{ public: Parent(); ~Parent(); }; Parent::Parent() { cout << "父类被构造" << endl; } Parent::~Parent() { cout << "父类被析构" << endl; } class Child: public Parent{ public: Child(); ~Child(); }; Child::Child() { cout << "子类被构造" << endl; } // 自动调用父类默认构造函数 Child::~Child() { cout << "子类被析构" << endl; } int main(){ Parent *p = new Child(); delete p; // 指针类型为基类指针,所以只调用了父类析构函数,不调用子类 }
输出:
父类被构造
子类被构造
父类被析构要使子类析构函数在delete时被调用,需要将基类析构函数声明为虚函数,这样delete时就是根据指针指向的对象查询虚函数表来调用析构函数而不是指针类型来调用
class Parent{ public: Parent(); virtual ~Parent(); }; Parent::Parent() { cout << "父类被构造" << endl; } Parent::~Parent() { cout << "父类被析构" << endl; } class Child: public Parent{ public: Child(); ~Child(); }; Child::Child() { cout << "子类被构造" << endl; } // 自动调用父类默认构造函数 Child::~Child() { cout << "子类被析构" << endl; } // 自动调用父类析构函数 int main(){ Child *p = new Child(); delete p; }
输出:
父类被构造
子类被构造
子类被析构
父类被析构
虚函数表和通过指针访问成员函数
虚函数和普通成员函数存在一起,被类的实例共享,不同的是编译期间会给含虚函数的基类创建虚函数表放在程序代码区,表内是虚函数地址,且虚函数表的指针会储存在对象内存中;派生类则是在基类的虚函数表上进行覆盖和新增
- 存在虚函数的基类对象的内存
虚函数表指针在对象内存最前面
- 单继承且派生类虚函数覆盖基类虚函数
虚函数表中,派生类虚函数覆盖基类虚函数 - 单继承且派生类存在属于自己的虚函数
虚函数表中,派生类虚函数位于基类虚函数后面
- 多继承时派生类对象内存:
每个含虚函数的基类都各有一张虚函数表,派生类会对每张表中函数原型相同的进行覆盖,但其私有的虚函数只在第一张表后面进行添加,便于查询
通过类指针访问成员函数:如果函数不是虚函数,则按照指针类型查找,因为成员函数和类绑定;如果函数是虚函数,则根据指针指向的对象查询,因为虚函数表的指针存在对象内存中,并且虚函数表中以及完成函数覆盖
基类指针访问派生类私有虚函数:基类指针无法直接访问派生类私有虚函数(违背指针语法),需要通过虚函数表的地址访问
class Base {
public:
virtual void func1() {
cout << "is base func1" << endl;
}
};
class Drived : public Base {
public:
virtual void func1() {
cout << "is drived func1" << endl;
}
virtual void func2() {
cout << "is drived func2" << endl;
}
};
typedef void(*Func)();
int main() {
Base *p = new Drived;
// p->func2(); p是Base *类型,访问不属于Base类的func2违背语法
Func f = (Func)*((int *)*(int *)p+1); // (int *)*(int *)p是第一个虚函数的指针
f();
}
纯虚函数和抽象类
virtual 返回值类型 函数名(参数列表) = 0;
通过在虚函数声明后加=0
(表示没有函数体),将其声明为纯虚函数。含纯虚函数声明的类即为抽象类,由于纯虚函数没有函数体,所以抽象类无法实例化,它只能作为基类,让派生类去实现纯虚函数
//线
class Line{ # 抽象类
public:
Line(float len);
virtual float area() = 0;
protected:
float m_len;
};
Line::Line(float len): m_len(len){ }
//矩形
class Rec: public Line{
public:
Rec(float len, float width);
float area(); # 实现了父类中的纯虚函数(遮蔽)
protected:
float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }
int main(){
Line *p = new Rec(10, 20);
cout<<"The area of Cuboid is "<<p->area()<<endl;
}
在实际开发中,可以定义一个抽象基类,只完成部分功能,剩下的功能(在基类中无法实现或者基类中不需要)强制要求派生类完成
typeid()与动态绑定
typeid(dataType)
typeid(expression)
typeid操作符会把获取到的类型信息放到type_info类型的对象里,并返回对象的常量引用;当需要具体类型信息是可通过其成员函数name等提取
静态绑定在编译连接阶段完成,如函数重载,一旦绑定完成就不会改变;
动态绑定在程序运行阶段完成,c++中唯一的动态绑定就是基类指针的多态,通过typeid可以获取基类指针的类型和它实际指向的对象的类型
class base
{
public :
virtual void m(){cout<<"base"<<endl;}
};
class derived : public base
{
public:
void m(){cout<<"derived"<<endl;}
};
int main(){
base *p = new derived;
cout << typeid(*p).name() << endl; //drived
cout << typeid(p).name() << endl; //base
}
注意如果没有构成多态,则typeid(*p)返回的是基类指针的类型:
class base
{
public :
void m(){cout<<"base"<<endl;} // 没有虚函数
};
class derived : public base
{
public:
void m(){cout<<"derived"<<endl;}
};
int main(){
base *p = new derived; // 没有构成多态,p只能访问到基类成员函数
cout << typeid(*p).name() << endl; //base
cout << typeid(p).name() << endl; //base
}
python
class Language:
def say(self):
print('language')
class Python(Language):
def say(self):
print('python')
a = Language()
a.say() #language
a = Python()
a.say() #python
由于a
所指代的对象不同以及子类对父类方法的重写,同一语句a.say()
的功能不同,这就是多态
多态条件:
- 存在继承关系
- 子类对父类同名成员覆盖
进阶“鸭子模型”(更灵活):不用再修改a
所指代的对象
class Whosay:
def say(self, who): #传入who参数的是哪个类的实例,who.say()就调用哪个类的say()方法
who.say()
class Language:
def say(self):
print('language')
class Python(Language):
def say(self):
print('python')
a = Whosay()
a.say(Language()) #language
a.say(Python()) #python
运算符重载
c++
基础
函数重载让一个函数名根据不同入参有不同功能;
运算符运行的本质是函数调用,运算符重载的本质是函数重载,函数重载让一个运算符根据不同类的对象有不同功能;c++本身就对运算符实现了重载但也允许程序员自己进行重载
运算符重载格式:
返回值类型 operator 运算符名称 (形参列表){
//todo
}
operator 运算符名称
可视为函数名,除了命名不同其他和普通函数没有区别
-
运算符重载函数作为类的成员函数
下面通过运算符重载,使+
可以实现新增Complex
类的加法运算class Complex{ private: double m_real; double m_imag; public: Complex(double real, double imag); // 入参为对象,所占内存空间大,所以用引用减少copy时的空间开销;声明为const防止入参在函数中被修改 // 声明为cons成员函数防止函数内修改对象 Complex operator+(const Complex &a) const; void display(); }; Complex::Complex(double real, double imag):m_real(real), m_imag(imag){} Complex Complex::operator+(const Complex &a) const{ return complex(this->m_real + A.m_real, this->m_imag + A.m_imag);; // 不要返回局部变量的引用或指针,虽然copy引用时省空间但是局部变量在函数调用结束时会销毁 } void Complex::display() { cout << m_real << "+" << m_imag << "i" << endl; } int main() { Complex c1(1, 2); Complex c2(1, 1); (c1 + c2).display(); //2+3i }
在执行
c1 + c2
语句时,+
(具有左结合性)检测到左边是Complex
对象,就会调用成员函数operater+()
,也就是转化为:c1.operater+(c2);
-
运算符重载函数作为全局函数
通过友元函数在全局范围内重载运算符class Complex{ private: double m_real; double m_imag; public: Complex(double real, double imag); friend Complex operator+(const Complex &a, const Complex &b); void display(); }; Complex::Complex(double real, double imag):m_real(real), m_imag(imag){} void Complex::display() { cout << m_real << "+" << m_imag << "i" << endl; } // 通过友元,全局函数可以访问入参对象的private成员 Complex operator+(const Complex &a, const Complex &b){ return Complex(a.m_real+b.m_real, a.m_imag+b.m_imag); } int main() { Complex c1(1, 2); Complex c2(1, 1); (c1 + c2).display(); //2+3i }
执行
c1 + c2
语句时,+
检测到两边都是Complex
对象时,就会调用全局函数operator+()
,也就是转换为:operator+(c1, c2);
运算符重载注意事项
-
不是所有运算符都能重载
能重载的有:
+ - * / % ^ & | ~ ! = < > += -= = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ – , -> -> () [] new new[] delete delete[]
不能重载的有:
长度运算符sizeof
、条件运算符:?
、成员选择符.
和域解析运算符::
-
重载不会改变运算符的用法、优先级、结合性
-
重载应尽量保留运算符原本的特性,否则没有意义,不如直接添加普通函数
-
运算符重载函数可以作为类的成员函数也可以作为全局函数
运算符重载函数作为类的成员函数时:- 元运算符的参数只有一个,一元运算符不需要参数
- 箭头运算符
->
、下标运算符[]
、函数调用运算符()
、赋值运算符=
只能以成员函数的形式重载
运算符重载函数作为全局函数时:
- 二元操作符就需要两个参数,一元操作符需要一个参数
- 并且参数中至少有一个需要是自定义的对象,否则和内置的运算符重载函数会产生二义性
int operator + (int a,int b){ // 无法通过编译 return (a-b); }
-
如何选择以全局函数还是成员函数的形式重载
以全局函数的形式重载运算符,是为了保证运算符的操作数被对称地处理class Complex{ private: double m_real; double m_imag; public: Complex() : m_real(0.0), m_imag(0.0){}; Complex(double real) : m_real(real), m_imag(0.0){}; Complex(double real, double imag); friend Complex operator+(const Complex &a, const Complex &b); void display(); }; Complex::Complex(double real, double imag):m_real(real), m_imag(imag){} Complex operator+(const Complex &a, const Complex &b){ return Complex(a.m_real+b.m_real, a.m_imag+b.m_imag); } void Complex::display() { cout << m_real << "+" << m_imag << "i" << endl; } int main() { Complex c1(1, 2); Complex c2(1, 1); (c1 + 1).display(); //2+3i }
无论是
c1+1
还是1+c1
,1
都会被自动转为Complex
,然后以operator+(c1, Complex(1))
或operateor(Complex(1), c1)
,正确运行以成员函数形式重载运算符时则无法对称进行,
c1+1
被转为c1.operator+(Complex(1))
正确运行,也就是说编译器对运算符右边的操作数没有严格数据类型要求;而1+c1
被转为(1).operator+(c1)
报错,编译器要求运算符左边的操作数为调用成员函数的对象,必须符合类的要求(不会进行自动类型转换)所以,运算符操作数非对称时优先选择以成员函数的形式重载;对称时优先选择以全局函数的形式重载
重载>>和<<
c++标准库本身已经对移位符<<
和>>
分别进行了成员函数形式的重载(cin和cout是内置对象),使之具有输入输出功能,如cin>>x;
等价于cin.operator>>(x);
,但其输入输出对象只能是c++内置的数据类型(如bool, int, double…)和标准库包含的类类型(string, ofstream, ifstream…)
下面以自定义的complex类为例,以全局函数形式进行<<
和>>
运算符重载(如果以成员函数形式重载则需要修改标准库中的istream和ostream类),使复数的输入输出就和int等基本类型一样简单
class Complex{
private:
double m_real;
double m_imag;
public:
Complex() : m_real(0), m_imag(0){};
Complex(double real, double imag);
friend istream &operator>>(istream &in, Complex &c);
friend ostream &operator<<(ostream &out, Complex &c);
};
Complex::Complex(double real, double imag):m_real(real), m_imag(imag){}
istream &operator>>(istream &in, Complex &c){
in >> c.m_real >> c.m_imag;
return in; //返回istream引用是为了能连续读取复数
}
ostream &operator<<(ostream &out, Complex &c){
out << c.m_real << "+" << c.m_imag << "i" << endl;
return out; //返回ostream引用是为了能连续输出复数
}
int main() {
Complex c1, c2;
cin >> c1 >> c2;
cout << c1 << c2 << endl;
}
cin >> c1
可转为cin.operator>>(c1)
和operator>>(cin, c1)
,前者在istream类里找不到匹配的成员函数,后者则和新增的运算符重载函数匹配了
重载[]
c++规定下标运算符[]
必须以成员函数形式重载,重载函数在类中的声明格式有两种:
返回值类型 & operator[] (参数);
和
const 返回值类型 & operator[] (参数) const;
第一种方式[]
不仅可以访问元素还可以修改元素,第二种只能访问元素。在实际开发中应该同时提供以上两种形式,第二种主要是限制权限为只读,并适应const对象(const对象只能调用const成员函数)
下面自定义Array类实现变成数组,并重载[]
class Array{
private:
int m_length;
int *m_p;
public:
Array(int length);
~Array();
int &operator[](int i);
const int &operator[](int i) const; //第一个const为了防止m_p[i]被赋值,保障只读不可写;第二个const是为了const对象可以调用
};
Array::Array(int length):m_length(length){
if (length == 0){
m_p = NULL;
}else{
m_p = new int[length];
}
}
Array::~Array(){
delete[] m_p; //对象销毁时只会销毁m_p指针本身,不会销毁它指向的堆上的内存,需要手动销毁
}
int &Array::operator[](int i) { return m_p[i]; }
const int &Array::operator[](int i) const { return m_p[i]; }
int main() {
Array a(3);
cin >> a[0];
cout << a[0] << endl;
const Array b(4);
cout << b[3] << endl;
}
重载++和–
不存在操作数对称性,所以优先以成员函数形式重载;
自增++
和自减--
的前置/后置形式都可以被重载
以计时器为例,实现++
重载:
class Watch{
private:
int m_min;
int m_sec;
public:
Watch();
Watch run();
void setzero();
Watch operator++(); // ++的前置形式
Watch operator++(int n); // ++的后置形式,入参n没有意义,只是为了区分前置和后置
friend ostream &operator<<(ostream &out, Watch &w);
};
Watch::Watch():m_min(0), m_sec(0){}
void Watch::setzero() {
m_min = 0;
m_sec = 0;
}
Watch Watch::run(){ // 自增和自减的返回值最好不要是引用类型,防止通过a++返回值修改了a本身
m_sec++;
if (m_sec == 60){
m_sec = 0;
m_min++;
}
return *this;
}
Watch Watch::operator++(){
return run();
}
Watch Watch::operator++(int n){ // ++后置:先返回对象本身,再自增
Watch w = *this;
run();
return w;
}
ostream &operator<<(ostream &out, Watch &w){
cout << w.m_min << ":" << w.m_sec;
return out;
}
int main() {
Watch w1, w2;
w2 = ++w1; // 自增前置
cout << w2 << endl;
cout << w1 << endl;
w1.setzero();
w2.setzero();
w2 = w1++; // 自增后置
cout << w2 << endl;
cout << w1 << endl;
}
0:1
0:1
0:0
0:1
重载强制类型转换运算符()
c++中类型名(类型名)
本身也是一种运算符,即类型强制转换运算符(如(int)a
强转double类型a
为int类型)
只能以成员函数的形式重载,使得强制类型转换对自定义的对象也可以进行
class Complex{
private:
double m_real;
double m_imag;
public:
Complex() : m_real(0), m_imag(0){};
Complex(double real, double imag);
operator double(); //强制转换符的重载函数不能指定返回值类型
};
Complex::Complex(double real, double imag):m_real(real), m_imag(imag){}
Complex::operator double() { return m_real; }
int main() {
cout << (double)Complex(1, 2) << endl; //1
}
(double)Complex(1, 2)
语句执行时,在检测到Complex(1, 2)
为Complex类后,就转化为Complex(1, 2).operator doule()
python
基础
python不支持函数重载,同一作用域中后面定义的函数会直接覆盖前面的同名函数,在命名空间中,每个函数名始终只有一个entry;
python中的运算符关于对象的实现是通过调用对象中特定的方法,那么运算符重载的本质则是通过对特定的方法进行实现(类里没有)或覆写(类里有默认的)使得类的实例对象支持python的各种内置操作,并自定义运算规则
内置操作(运算符) | 对应方法名 |
---|---|
在_init_前创建对象 | __new__ |
创建对象时初始化 | __init__ |
销毁对象时回收资源 | __del__ |
对象x做x+y | __add__ |
对象x做y+x | __radd__ |
对象x做x+=y | __iadd__ |
x|y | __or__ |
repr(x), str(x) | __repr__, __str__ |
获取类属性x.attr或getattr(x, name) | __getattr__ |
属性赋值x.attr=v或setattr(x, name, value) | __setattr__ |
删除属性del x.attr或delattr(x, name) | __delattr__ |
索引序列中的元素x[i]或x[i:j] | __getitem__ |
根据索引赋值x[i]或x[i:j]=sequence | __setitem__ |
根据索引删除序列中对应元素 del x[i]或x[i:j] | __delitem__ |
判断序列中是否包含某元素item in x | __contains__ |
len(x) | __len__ |
<,>,<=,>=.=,!= | __lt__, __gt__, __le__, __ge__, __eq__, __ne__ |
迭代环境下生成迭代器iter(x),与取下一条next() | __iter__, __next__ |
转为整数类型hex(x), bin(x), oct(x) | __index__ |
对类对象obj执行with obj as var时先调__enter__,结果传给var,结束前调__exit__ | __enter__, __exit__ |
函数调用x(*args, **kargs) | __call__ |
__dir__查看方法和属性
dir()函数传入对象/类,或通过对象调用__dir__方法,可以获取属性和方法(包括从父类继承的,但通过对象只能查看实例属性,通过类只能查看类属性),以列表形式输出
通过对__dir__的覆写可以自定义dir(obj)的输出内容,但是没有那个必要!
class Demo1:
pass
class Demo2:
def __dir__(self):
return ["i am demo"]
print(dir(Demo1())) // Demo1会继承object类的方法
print(dir(Demo2))
[‘_class__', '_delattr__’, ‘_dict__', '_dir__’, ‘_doc__', '_eq__’, ‘_format__', '_ge__’, ‘_getattribute__', '_gt__’, ‘_hash__', '_init__’, ‘_init_subclass__', '_le__’, ‘_lt__', '_module__’, ‘_ne__', '_new__’, ‘_reduce__', '_reduce_ex__’, ‘_repr__', '_setattr__’, ‘_sizeof__', '_str__’, ‘_subclasshook__', '_weakref__’]
[‘i am demo’]
__dict__查看属性名和属性值
使用类名调__dict__方法,可以获取类属性;使用对象名调__dict__方法,获取的是实例属性;并且子类__dict__不会包含父类__dict__
以字典形式输出属性名和属性值
class Demo:
cls = "class"
def __init__(self):
self.obj = "object"
print(Demo.__dict__) # 注意__dict__调用时不需要括号!
print(Demo().__dict__)
{‘_module__': '_main__’, ‘cls’: ‘class’, '_init__': <function Demo._init__ at 0x000002E6E2E35158>, ‘_dict__': <attribute '_dict__’ of ‘Demo’ objects>, ‘_weakref__': <attribute '_weakref__’ of ‘Demo’ objects>, ‘__doc__’: None}
{‘obj’: ‘object’}
借助由类实例对象调用 dict 属性获取的字典,可以使用字典的方式对其中实例属性的值进行修改
class Demo:
cls = "class"
def __init__(self):
self.obj = "object"
d = Demo()
d.__dict__['obj'] = 'new'
d.obj #new
__new__创建实例
__new__是负责创建类实例的静态方法,类有默认的__new__方法,但python也允许定义新的__new__覆盖默认的
覆写__new__时需要用super().__new__()来创建类
class demo:
instance_created = 0
def __new__(cls, attr):
print("new:", attr)
instance = super().__new__(cls)
instance.index = cls.instance_created
cls.instance_created += 1
return instance
def __init__(self, attr):
print("init:", attr)
test = demo('abc')
print(demo.instance_created, test.index)
覆写__new__可以返回不同类的实例,此时会跳过对__init__的调用,在修改不可变类的对象创建行为时,可以利用这一点
class nonZero(int): #继承不可变类int
def __new__(cls,value):
return super().__new__(cls) if value != 0 else None
def __init__(self,skipped_value):
print("__init__()")
super().__init__()
print(type(nonZero(-12)))
print(type(nonZero(0))) #入参为0时,实例化为None
__init__()
<class ‘__main__.nonZero’>
<class ‘NoneType’>
注意,__new__一般只在MetaClass中覆写,因为__new__的覆写会破坏“init() 中执行所有初始化工作”的潜规则,并且由于__new__不限于返回同一个类的实例,很容易被滥用
__repr__自我介绍
__repr__是对象用来做自我介绍的方法,默认情况下obj
输出字符串“类名+object at+内存地址”,但对__repr__覆写后,可以输出自定义的自我描述性质
class Demo1:
pass
class Demo2:
def __repr__(self):
return "i am demo"
print(Demo1()) #<__main__.Demo1 object at 0x000002E6E2DF6D68>
print(Demo2()) #i am demo
setattr(), getattr(), hasattr()根据属性或方法名字str进行查询、修改
1) hasattr()
用来判断对象是否包含指定名字的属性或方法,返回bool类型
hasattri(obj, name)
class demo:
pass
hasattr(demo(), "__init__") #True
2)getattr()功能类似obj.attr
用来获取对象中指定属性的值或方法的信息,包括从父类继承的成员
对应类方法为__getattribute__(必然被调用;类里有默认的,也可以覆写)和__getattr__(属性或方法通过__getattribute__没有查询到后调用)
getattr(obj, name, [default])
如果不指定 default 参数,则程序将直接报AttributeError错误,反之该函数将返回 default 指定的值
class demo:
name = 'xx'
def __init__(self):
self.age = 11
def say():
print("demo")
obj = demo()
print(getattr(obj, "say")) #<bound method demo.say of <__main__.demo object at 0x000002E6E4A1AA90>>
print(getattr(obj, "name")) #xx
print(getattr(obj, "age")) #11
print(getattr(obj, "none", "no such attr")) #no such attr
3)setattr()功能类似obj.attr=v
setattr(obj, name, value)
对应方法为__setattr__,类里有默认的,也可以覆写
- 用于修改对象中的成员值
- 用于给实例对象动态添加属性或方法
- 可以将属性修改为方法或将方法修改为属性
对应类方法为__setattr__
class demo:
def __init__(self):
self.name = 'xxx'
def say():
print("add method")
d = demo()
setattr(d, "name", 'tom')
print(d.name) #tom
setattr(d, "say", say) #通过对象添加的方式只属于该对象
d.say() #add method
setattr(d, 'name', say) #将属性修改为方法
d.name() #add method
__call__函数调用
实现__call__方法使得对象名()
可转为对象名.__call__()
,实例对象也就是成为了可调用对象(和函数一样)
class demo:
def __call__(self, name):
print("调用:", name)
d = demo()
d("call") #调用: call
用__call__可以弥补hasattr()函数无法判断指定的名字是属性还是方法的缺陷,因为只有可调用对象才有__call__方法(方法有,属性没有)
class demo:
def __init__(self):
self.name = 'xxx'
def say():
print("demo")
d = demo()
if hasattr(d, 'name'):
print(hasattr(d.name, "__call__")) #False
if hasattr(d, 'say'):
print(hasattr(d.say, "__call__")) #True
自定义序列
和序列相关的特殊方法:
方法名 | 对应运算 |
---|---|
__len__(self) | len(x) 获取序列中元素个数 |
__contains__(self, value) | item in x判断序列中是否包含某元素 |
__getitem__(self, key) | x[i]或x[i:j]索引序列中的元素 |
__setitem__(self, key, value) | x[i]或x[i:j]=seq根据索引赋值 |
__delitem__(self, key) | 根据索引删除序列中对应元素 |
例:自定义简易字典
class Mydict:
def __init__(self):
self.length = 0
self.step = 0
#重载运算符实现自定义序列
def __len__(self):
return self.length
def __setitem__(self, key, value):
self.__setattr__(key, value) #根据成员名str赋值,这里也可以用setattr()函数
def __getitem__(self, key):
return self.__getattribute__(key)
def __delitem__(self, key):
self.__delattr__(key)
def __contains__(self, value):
return value in self.__dict__.keys()
d = Mydict()
len(d)
d["key"] = "value"
print(d["key"]) #value
print("key" in d) #True
del d["key"]
print("value" in d) #False
迭代器
迭代指的是for item in x
遍历序列中元素的过程,其原理为:
x = [1, 2, 3]
# 将x转为迭代器
iteration = iter(x)
# for循环遍历迭代器的本质是不停调用 next(迭代器)
print(next(iteration)) #1
print(next(iteration)) #2
print(next(iteration)) #3
通过inter()
生成序列的迭代器后,用next()
获取迭代器中下一个元素
和迭代for item in x
相关的特殊方法:
方法名 | 对应运算 |
---|---|
__iter__(self) | 生成迭代器iter(x) |
__next__(self) | 去迭代中的下一条next(x) |
例:自定义迭代器实现字符串逆序输出
class Inverse:
def __init__(self, string:str):
self.__string = string
self.__step = len(string)
def __iter__(self):
return self
def __next__(self):
self.__step-=1
if self.__step < 0:
raise StopIteration
return self.__string[self.__step]
l = Inverse('123')
for ele in l:
print(ele, end='')
321
生成器
生成器和迭代器的区别在于:迭代器先把完整的序列放到容器里,然后循环不断输出下一个元素,而生成器是在循环的过程中生成下一个元素并输出,所以可以节省空间
# 构造生成器
g = (x+1 for x in range(3))
# 遍历生成器
for ele in g:
print(ele)
其本质是:
def generator():
for x in range(3):
yield x+1
# 构造生成器
g = generator() # 生成器的本质是个函数,而不是一个输入了完整序列的容器(迭代器)
# 遍历生成器
print(next(g))
print(next(g))
print(next(g))
yield
关键字会返回相应的值并且暂停生成器函数的执行,直到下一个next(生成器)
或生成器.__next__()
出现,再继续从生成器函数刚才暂停的地方开始往下执行
可以通过list()或tuple()将生成器生成的所有值存储成列表或元组,其底层实现和for循环遍历生成器是类似的
g = (x+1 for x in range(3))
list(g) #[1, 2, 3]
装饰器
用funcA(fn)去装置funB():
def funcA(f):
f()
@funcA
def funcB():
print("funcB")
等价于:把funcB作为参数传给funcA(),再将funcA()执行的返回值反馈给funcB
def funcA(f):
f() # 执行传入的f参数
def funcB():
print("funcB")
funcB = funcA(funcB)
如果被修饰的funcB含参数,那么需要在函数装饰器中嵌套一个函数获取funcB的参数,该函数带有的参数个数和被装饰器修饰的函数相同,或使用*args 和 **kwargs变长参数作为内嵌函数参数
def funcA(f):
def say(arc):
f(arc)
return say
@funcA
def funcB(arc):
print("funcB:", arc)
funcB("test")
等价于:
def funcA(f):
def say(arc):
f(arc)
return say
def funcB(arc):
print("funcB:", arc)
funcB = funcA(funcB)
funcB("test")