面向对象程序设计(OOP)
基本概念
- 面向对象程序设计(Object-oriented Programming)
- 对象/类(Object&Class)
- 继承(Inheritance)
- 多态(Polymorphism)
- 绑定(Binding)
对象属于值的范畴,是程序动态运行时刻的实体;类则属于类型的范畴,属于静态编译时刻的实体。
类(class)
对象(Object)构成了面向对象程序的基本计算单位,而对象的特征则由相应的类(Class)来描述。
对象是用类来创建的。
class <类名> { <成员描述> } ;
class中分为
- public:允许外部访问(构成class与外界的接口)
- private:只能在本类和友元的代码中访问
- protected:只能在本类、派生类和友元的代码中访问
- 默认为private
Tips
在类中声明一个数据成员的类型时,如果未见到相应类型的定义或相应的类型未定义完,则该数据成员的类型只能是这些类型的指针或引用类型。(也就是必须知道该class所需要的内存大小)
例如:
class A; //A是在程序其它地方定义的类,这里是声明。
class B{
A a; //Error,未见A的定义(只有声明)。
B b; //Error,B还未定义完,递归了!
A *p; //OK, 因为指针只是一个地址,大小已知。
B *q; //OK
A &aa; //OK,同指针。
B &bb; //OK
};
1 | class Date{ //定义Date类 |
成员函数的实现(函数体)可以放在类定义中,例如:
class A
{ …
void f() {…} //建议编译器按内联函数处理
};
成员函数的实现也可以放在类定义外,例如:
class A
{ …
void f(); //声明
};
void A::f() { … } //要用类名受限,区别于非成员函数(全局函数)
对象(Object)
创建方式
1 | class A{ |
1 | A a1; //创建一个A类的对象 |
对象在进入相应变量的生存期时创建,通过变量名来标识和访问。相应变量的生存期结束时,对象消亡。(栈)
通过用new操作来创建对象,用delete操作来撤消(使之消亡)。对象通过指针来标识和访问。(堆)
单个动态对象的创建与撤消
1
2
3
4A *p; //创建一个叫做p的指向 A 类型的指针
p = new A; //创建一个A类的动态对象(堆上),并将该指针赋值给p,此时p指向的是一个在堆上创建的A类对象
… *p … //或,p->...,通过p访问动态对象
delete p; // 撤消p所指向的动态对象动态对象数组的创建与撤消
1
2
3
4A *q;
q = new A[100]; //创建一个动态对象数组,将指向数组首元素的指针赋值给q。
...q[i]... //或,*(q+i),访问动态对象数组中的第i个对象
delete []q; //撤消q所指向的动态对象数组
成员对象
- 一个成员对象可以包含另一个对象(称为成员对象)
对象操作
1 | class A{ |
class外部访问class内private成员受到限制
可以对同类对象进行赋值
Date yesterday, today, some_day;some_day = yesterday; //默认是把对象yesterday的数据成员分别赋值给对象some_day的相应数据成员取对象地址
Date today;Date *p_date;p_date = &today; //把对象today的地址赋值给指针p_date把对象作为参数传给函数
1
2
3
4
5
6
7void f(Date d) //创建一个新对象d,其数据成员用实参对象的数据成员对其初始化
{ ...... }
void g(Date &d) //不创建新对象,d就是实参对象!
{ ...... }
Date today;
f(today); //调用函数f,copy一份,对象today不会被f修改
g(today); //调用函数g,对象today会被g修改!把对象作为函数的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Date f(Date &d){
d.print(); //输出:2020.2.20
return d; //创建一个临时对象作为返回值,用d对其初始化
}
Date& g(Date &d){
d.print(); //输出:2020.2.20
return d; //不创建新对象,把对象d作为返回值
//切记:不能返回临时本地变量引用
}
Date some_day; //创建一个日期对象
some_day.set(2020,2,20);
f(some_day).set(2017,3,13);
some_day.print();//前者显示:2020.2.20,因为修改的是临时对象
g(some_day).set(2017,3,13);
some_day.print();//后者显示:2017.3.13,因为修改的是some_day
构造函数(对象的初始化)
构造函数
class的特殊成员函数,名字与class名字相同,无返回值类型
创建对象时,构造函数会被自动调用
1
2
3
4
5
6
7
8class A{
int x,y;
public:
A() { x = 0; y = 0; } //构造函数
......
};
A a; //创建对象a:为a分配内存空间,然后调用A类的构造函数A()。构造函数可以重载
创建对象时,没有指定哪一个构造函数,则默认,也可以显式调用特定构造函数
A a1(1) //显式调用A(int i);通过类的构造函数创建临时对象
A a;a = A(10); //创建一个临时对象并把它赋值给a对象创建后不能再调用构造函数
A a;a.A(1); //Error!
成员初始化表
成员变量的初始化顺序仅与其定义顺序相关,与初始化列表顺序无关;而且初始化列表先于构造函数体执行
1 | class A{ |
就地(内部)初始化
1 | class A{ |
执行次序:就地初始化 => 初始化列表 => 构造函数。
析构函数
- 名字为“
~<类名>”,没有返回类型、不带参数、不能被重载。 - 一个对象消亡时,系统在收回它的内存空间之前,将会自动调用对象类中的析构函数。可以在析构函数中完成对象被删除前的一些清理工作。
- 一般情况下,类中不需要自定义析构函数,但如果对象创建后,自己又额外申请了资源(如:额外申请了内存空间),则可以自定义析构函数来归还它们。
1 | class String{ |
可以显式调用,不是让对象消亡,而是暂时归还对象额外申请的资源。
1 | String s1("abcd"); |
成员对象初始化和消亡处理的次序
创建包含成员对象的对象时,
- 先执行成员对象类的构造函数,再执行本对象类的构造函数。
- 若包含多个成员对象,这些成员对象的构造函数执行次序则按它们在本对象类中的说明次序进行。
- 从实现上说,
- 是先调用本身类的构造函数,但在进入函数体之前,会去调用成员对象类的构造函数,然后再执行本身类构造函数的函数体!
- 也就是说,构造函数的成员初始化表(即使没显式给出)中有对成员对象类的构造函数的调用代码。
- 注意:如果类中未提供任何构造函数,但它包含成员对象,则编译程序会隐式地为之提供一个默认构造函数,其作用就是调用成员对象类的构造函数!
对象消亡时,
- 先执行本身类的析构函数,再执行成员对象类的析构函数。
- 如果有多个成员对象,则成员对象析构函数的执行次序则按它们在本对象类中的说明次序的逆序进行。
- 从实现上说,
- 是先调用本身类的析构函数,本身类析构函数的函数体执行完之后,再去调用成员对象类的析构函数!
- 也就是说,析构函数的函数体最后有对成员对象类的析构函数的调用代码!
- 注意:如果类中未提供析构函数,但它包含成员对象,则编译程序会隐式地为之提供一个析构函数,其作用就是调用成员对象类的析构函数。
“this”指针
类的每一个成员函数(静态成员函数除外)都有一个隐藏的形参this,其类型为该类对象的指针;在成员函数中对类成员的访问是通过this来进行的。
对于前面A类的成员函数g:
void g(int i) { x = i;}编译程序将会把它编译成:
void g(A *const this, int i) { this->x = i;};
拷贝构造函数
若一个构造函数的参数类型为本类的引用,则称它为拷贝构造函数。
1
2
3
4
5
6class A{
......
public:
A(); //默认构造函数
A(const A& a); //拷贝构造函数
};在三种情况下,会调用类的拷贝构造函数:
创建对象时显式指出。
A a1;A a2(a1); //创建对象a2,用对象a1初始化对象a2
把对象作为值参数传给函数时。
把对象作为函数的返回值时。
例如,在下面的类中没有自定义拷贝构造函数:
系统提供的隐式拷贝构造函数将会使得s1和s2的成员指针str指向同一块内存区域!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class String{
int len;
char *str;
public:
String(char *s) {
len = strlen(s);
str = new char[len+1];
strcpy(str,s);
}
~String() { delete []str; len=0; str=NULL; }
};
......
String s1("abcd");
String s2(s1);隐式拷贝构造函数实现的是一种浅拷贝,需要定义拷贝构造函数实现深拷贝
1
2
3
4
5String::String(const String& s){
len = s.len;
str = new char[len+1];
strcpy(str,s.str);
}
常成员函数及静态成员
常成员函数
只读不写
int get_day() const; //常成员函数只约束对数据成员值的修改,指针所指的地址赋值不报错
常量对象只能调用对象类中的常成员函数
静态数据成员
实现同一个类的不同对象之间的数据共享
所有对象共用一个数据成员
在类之外进行初始化
static int x; //静态数据成员声明int A::x = 0; //初始化
静态成员函数
- 只能访问静态数据成员
- 没有隐藏的this参数
1 | class A{ |
- 通过对象访问:
cout << a.get_shared(); - 通过类直接访问:
cout << A::get_shared();
友元
在类中用
friend显式指出1
2
3
4
5
6
7
8class A{
friend void func(); //全局函数func可访问x
friend class B; //类B的所有成员函数可访问x
friend void C::f(); //类C的成员函数f可访问x
private:
int x;
};具有不对称性:A是B的友元,B不是A的友元
不具有传递性:C是B的友元,B是A的友元,C不是A的友元
类作为模块
1 | //A.h(模块接口) |
继承-派生类(Inheritance)
- 基类、派生类
- 派生类拥有基类的所有成员,并可以
- 定义新的成员
- 对基类的一些成员进行重定义(复写 override)
- 继承分为
- 单继承:一个类有一个直接基类
- 多继承:一个类有多个直接基类
单继承
- <派生类名>为派生类的名字。
- <基类名>为直接基类的名字。
- <成员说明表>是在派生类中新定义的成员,其中包括对基类成员的重定义。
- <继承方式>用于指出从基类继承来的成员在派生类中对外的访问控制
1 | class <派生类名>: <继承方式> <基类名> |
1 | class A{ //基类 |
定义派生类时要见到基类的定义
派生类中访问基类成员
不能直接访问基类的private成员
protected的数据成员可以被派生类访问
派生类的作用域可以理解为嵌套在基类中(复写会覆盖)
若同名函数,则基类中的被隐藏
f(); //派生类中A::f(); //访问基类函数
基类成员在派生类中对外的访问控制
继承方式
- public、private、protected
- 默认private
取最严格控制的作为继承类的访问控制
| 继承方式\派生类访问控制\基类访问控制 | public | private | protected |
|---|---|---|---|
| public | public | 不可直接访问 | protected |
| private | private | 不可直接访问 | private |
| protected | protected | 不可直接访问 | protected |
继承方式调整
1 | class A{ |
子类型
对用类型T表达的所有程序P,当用类型S去替换程序P中的所有的类型T时,程序P的功能不变,则称类型S是类型T的子类型
把以public方式继承的派生类看作是基类的子类型
1 | class A{ //基类 |
允许多余,不允许空白
派生类对象的初始化和消亡处理
派生类对象的初始化由基类和派生类共同完成
从基类继承的数据成员由基类的构造函数初始化;
派生类的数据成员由派生类的构造函数初始化。
当创建派生类的对象时
- 先执行基类的构造函数,再执行派生类构造函数
- 默认情况下,调用基类的默认构造函数,如果要调用基类的非默认构造函数,则必须在派生类构造函数的成员初始化表中指出
当派生类对象消亡时
- 先调用本身类的析构函数,执行完后会自动去调用基类的析构函数
如果一个类D既有基类B、又有成员对象类M,则
在创建D类对象时,构造函数的执行次序为:
B->M->D
当D类的对象消亡时,析构函数的执行次序为:
D->M->B
多继承
1 | class <派生类名>: [<继承方式>] <基类名1>, |
聚合(aggregation)/ 组合(composition)
聚合
1 | class A { ...... }; |
组合
1 | class A |
虚函数与消息的动态绑定(多态)
1 | class A{ |
静态绑定:只看形参的调用(c++默认)
1 | void func1(A& x) |
动态绑定(虚函数)
派生类中复写f(),希望在通过对象来决定是调用A::f还是B::f
用虚函数来实现动态绑定(根据实参调用)virtual void f();
1 | class A |
可以另外重载virtual void f(); //class A
void f(); //class B,虚函数重定义
void f(int); //class B,新定义的重载
只有通过指针或引用访问对象类的虚函数时才进行动态绑定
动态绑定的各种情况
1 | class A |
抽象类
纯虚函数
不给实现的虚函数
virtual int f()=0;
包含纯虚函数的类成为抽象类
作为接口
- Title: 面向对象程序设计(OOP)
- Author: SyEic_L
- Created at : 2025-02-19 20:42:19
- Updated at : 2025-03-19 11:57:50
- Link: https://blog.syeicl.vip/2025/02/19/面向对象程序设计/
- License: This work is licensed under CC BY-NC-SA 4.0.