C++高级程序设计-学习笔记-OOP
C++高级程序设计-学习笔记-OOP
现在应该已经都忘了….对我个人而言,正确的方式应该是边做真实项目边学。科研和工程都是这样
Lecture1 面向对象OOP入门
- 为什么学了javaOO还要学c++OO?
高级程序设计
- 最大的差别:封装。
函数放到了class结构体中,形成抽象数据类型ADT,分为属性和行为。
数据被保护在内部,尽可能地保留细节,只保留外部接口。
减少数据间的耦合
- 默认带有this参数,指向classw
0.Concepts
Program = Object1 + … + ObjectN
Object = Data + Operation
Message : function call
面向对象 Object-Oriented
基于对象 Object-Based (Ada)
- 没有继承 without Inheritance
好处:
对于外部:
对于内部:
1.类的构成
函数声明和定义分开
(A.h声明和A.cpp定义分开)
1 |
|
inline : 函数声明和定义合起来,内联函数
1 |
|
- 毕竟在内存里,private其实仍可以调用
对象的三种存储位置
一个由C/C++编译的程序占用的内存分为以下几个部分
- 栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
- 文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放
- 程序代码区—存放函数体的二进制代码。
全局静态
class外声明
栈区
class内直接声明Tdate t,不需要new(?)
堆区
指针指向的new(?)
Tdate *p = new Tdate;
p->Setdate();
频繁调用场合并不适合new,就像new申请和释放内存一样。
2.构造函数
对象的初始化,无返回类型,自动调用不可直接调用。可重载
- 编译系统自动提供默认构造函数,无参数
对象数组的构造:
A a[4]; //调用a[0]、a[1]、a[2]、a[3]的A()
A b[5]={ A(), A(1), A(“abcd”), 2, “xyz“ };
成员初始化表
- 构造函数的补充
- 先于构造函数
- 按类数据成员申明次序
1 |
|
- 应优先使用初始化表代替赋值动作
- const 成员/reference 成员/对象成员
好处:减轻Compiler负担,效率高(二次->一次)
坏处:数据成员太多时,不采用本条准则。降低可维护性
析构函数(待深入)
定义:~NewObject()
对象消亡时,系统自动调用
- 一般为public
- 也可定义为private,此时不会自动调用,强制自主控制对象存储分配
拷贝构造函数(待深入)
浅拷贝:将 a 和 obj1 所在内存中的数据按照二进制位(Bit)复制到 b 和 obj2 所在的内存,这种默认的拷贝行为就是浅拷贝
深拷贝:将对象所持有的其它资源一并拷贝的行为叫做深拷贝,我们必须显式地定义拷贝构造函数才能达到深拷贝的目的。
定义:B(const B& b)(B引用类型的b)
创建对象时,用一同类的对象对其初始化
自动调用
默认拷贝构造函数:
默认调用成员对象的拷贝构造函数,逐个成员拷贝(member-wise initialization)
自定义拷贝构造函数:
默认调用成员对象的默认构造函数(不拷贝了,让程序员指导)
此例子中,调用了自定义拷贝构造函数
若删去红色,则为蓝色结果。
移动构造函数(待深入)
定义:A(A& &)
目的:移动对象,减轻拷贝成本,加快效率
拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,并且进行了深拷贝,就需要给对象分配地址空间。而移动构造函数就是为了解决这个拷贝开销而产生的。移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。
- 左值、右值:无明确定义。一般指运算符左右的变量和值
- 引用即变量的别名
不自定义拷贝构造函数和析构函数时,编译器不会自动合成移动构造函数。
(要么三个默认,要么全自定义)
2.5 左值引用和右值引用
左值定义:
int a = 10;
int &var = a;
var = 20;
右值定义:
int &&var = 10;
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
与指针的区别:
左值引用直接用“.”解引用
指针需要用“->”
从指令层面来说,没有指针和引用之分,他们都是在地址层面的操作。
也就是说,在我们定义一个引用的时候,底层实际上就是定义了一个指针,只不过在使用的时候会自动加上解引用。
有这么一种说法:引用是一种更安全的指针。
而为什么引用比指针安全,这就要提到他们的第一个区别:引用是必须初始化的,而指针可以不初始化。 当我们使用引用的时候,我们可以保证它一定会引用一块内存,而指针会出现空指针和野指针的问题。
3.动态内存
动态对象
1.new、delete取代malloc和free的原因:对象的初始化和析构
new/delete 可看作操作符
在heap中创建
- p = new A做的事:
- 在heap中申请内存
- 调用A构造函数
- 返回对象地址,赋值给p
- delete p做的事:
- 调用p指向对象的析构函数
- 释放内存
- delete详解:
- delete后面接着指针,delete后建议顺手指向NULL(避免二次释放)
- delete void * 时,不会调用析构(c++重类型的特点)
2.堆上对象都是无名对象。必须有指针指向才能访问它们
动态数组
A *p;
p = new A[100];
delete []p;
注意:1.new不能显式初始化,必须有默认构造函数
2.delete中的[] 不能省略,否则只释放单个数组
多维数组创建
- 现多用一维数组模拟。[i * rows + j]
define row
define colomn
for循环new创建,
4.Const成员
不可改变。如const对象不可改变成员变量
初始化只能放在构造函数的成员初始化表中进行
和static不同
- const 定义的常量在超出其作用域之后其空间会被释放,而 static 定义的静态常量在函数执行后不会释放其存储空间。
- static 表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。而const 数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。
- 要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static const。
和define不同之处:编译阶段使用。而define只是宏替代字符
Const成员函数(const类下)(待深入)
class A……
const A a(0,0);
const成员函数定义:在函数声明后加const
void A::f() 不能通过编译
{ x = 1; y = 1; }
void A::show() const 能编译
{ cout << x << y ; }
目的:避免改变const成员变量,方便编译器检查报错。
const类内任何变量不可修改,但指向的内存地址可以修改
- 也可声明为mutable关键字,可修改const成员变量
静态成员
解决问题:同一个类的不同对象如何共享变量
类对象内部所共享
唯一拷贝
遵循类访问控制
静态成员函数:
- 只能存取静态成员变量,调用静态成员函数
静态成员的使用
- 通过对象使用
A a; a.f(); - 通过类使用
A :: f();
- 通过对象使用
Resource Control原则:谁创建,谁归还
const static:既全局又不可改,故直接在声明时初始化(不在初始化表)
友元
解决问题:类外部不能访问该类的private成员,而通过该类的public方法会降低对private成员的访问效率,缺乏灵活性。
- 分类
- 友元函数
- 友元函数是指某些虽然不是类成员函数却能够访问类的所有成员的函数。
- 友元类
- 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
- 友元类成员函数
- 使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员了。
- 友元函数
- 作用
- 提高程序设计灵活性
- 数据保护和对数据的存取效率之间的一个折中方案
- 友元不具有传递性
实例
void func() ; class B;//这种情况下B不是必须的 class C{ void f(); }; class A{ friend void func();//友元函数 friend class B; //友元类:B中的每一个函数都可以访问A的成员函数 friend void C::f();//友元类成员函数 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
* 友元类成员函数C一定要前声明,否则编译器无法判断内存(B不用前声明)
## 5.原则
* 迪米特法则
努力让接口**完满**且**最小化**
减少对象间依赖,只和最近的朋友交流
# Lecture 2 面向对象-继承
* 本质目的:对数据类型的区分
* 继承机制
* 基于目标代码的复用
* 对事物进行分类
* 派生类是基类的具体化
* 把事物(概念)以层次结构表示
* 增量开发
## 1.单继承
* 定义:单括号
class Undergraduate_Student : public Student
{}
* 可以访问父类的成员
* 继承方式
* public
* private
* **私有**成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。
* 重载virtual或使用基类中的protected成员
* 在设计层面无意义,只用于实现层面
* protected
* **保护**成员变量或函数与私有成员十分相似,但有一点不同,保护成员在派生类(即子类)中是可访问的。
c++是静态编译的
* 子类可以使用父类成员,从而修改父类成员权限,但只能从公开(public/private)修改为私有,不能从private修改为其他。
* public:
using son :: parent x
* 同名成员可以有多个版本,取决于当前的空间(由定义或::决定)
### 重写和隐藏
* ```
class Student {
int id;//id在Undergraduated_Student中仍然是私有的
public:
char nickname[16];
void set_ID (int x) {id = x;}
void SetNickName (char *s) {strcpy (nickname,s);}
void showInfo () {cout << nickname << ":" << id << endl ;}
void showInfo(int x){cout << x << endl;}
};
class Undergraduated_Student: public Student {
int dept_no;//学院编号
public:
void setDeptNo(int x){dept_no = x;}
void showInfo(){cout << dept_no << ":" << nickname << endl;}
void set_ID (int x) {……}
void showInfo(){
cout << dept_no << ":" << nickname << endl;
}
private:
Student::nickname;//这样在才能修改可见性
void SetNickName();//新定义了一个private方法,父类对应方法被隐藏
};
Undergraduated_Student us;
us.showInfo(10);//可以吗?不可以,因为是新的名空间,重定义后面的名空间访问不到派生类中的showInfo():Overwirtten **重写(绝对不是覆盖)**,隐藏基类的showInfo()函数
如果基类中有一个void ShowInfo(int x)那么是不是从基类可以进行调用呢?
- 不可以(所有被重写函数都被隐藏)
- 因为重定义将名空间进行了覆盖
父类中的所有的函数都不可见:但是我们可以通过指定名空间来完成访问:
using Student::showInfo
,所有的版本都可以见,这时候是重写。匹配不上是不会去别的名空间进行匹配(也就是不会去student那里去匹配)
友元和protected
友元只能访问当前子类,不能通过派生类访问基类
- 否则protect与public无区别
执行次序
- 派生类对象的初始化
- 由基类和派生类共同完成
- 构造函数的执行次序
- 基类的构造函数
派生类对象成员类的构造函数
派生类的构造函数
- 基类的构造函数
- 析构函数的执行次序
- 与构造函数相反
默认和自定义构造函数:
- 基类构造函数的调用
- 缺省执行基类默认构造函数
- 如果要执行基类的非默认构造函数,则必须在派生类构造函数的成员初始化表中指出
- B(int i, int j): A(i)
- 拷贝构造函数同理,需在派生类构造函数的成员初始化表中指出基类所选构造函数
2.虚函数
静态绑定
1 |
|
子不可变父(丢失信息),父可变子
以上均为前期绑定
- 静态绑定根据形参(前缀)决定
动态绑定
- 前期绑定(Early Binding)
- 编译时刻确定类型(看前缀)
依据对象的静态类型
效率高、灵活性差
- 编译时刻确定类型(看前缀)
- 动态绑定
- 运行时刻
依据对象的实际类型(动态)
灵活性高、效率低
- 运行时刻
默认前期绑定,后期绑定需显式指出:virtual
- 基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数
- 限制
- 类的成员函数才可以是虚函数
静态成员函数不能是虚函数
内联成员函数不能是虚函数
构造函数不能是虚函数
析构函数可以(往往)是虚函数
- 类的成员函数才可以是虚函数
虚函数表
虚函数表的原理:指针偏移
c++类定义虚函数后,会生成一个void**类型的指针(_vfptr),指向虚函数表的第一个虚函数。同一个类的不同实例共用同一份虚函数表, 它们都通过一个所谓的虚函数表指针vfptr指向该虚函数表。定义类对象时, 编译器自动将类对象的vfptr指向这个虚函数表。
**((char *)p-4)(p):调用A::f(this)
注意每一个函数在调用的时候都会传入一个const的this指针
空间上和时间上都付出了代价
空间:存储虚函数表指针和虚函数表
时间:需要通过虚函数表查找对应函数地址,多调用
纯虚函数
f() = 0,不给出函数实现
抽象类
- 至少包含一个纯虚函数
- 不能用于创建对象:抽象类类似一个接口,提供一个框架
- 为派生类提供框架,派生类提供抽象基类的所有成员函数的实现
多个函数
1 |
|
- 只有寻找虚函数一步是动态,其他都是静态编译
虚函数调用非虚就是非虚,所有版本默认调用this,静态编译.
非虚函数调用虚就是虚,根据对象实际类型动态确定
关键字
- 为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。
- 保留字override:当使用 override时,编译器会生成错误,而不会在不提示的情况下创建新的成员函数。防止漏写virtual
- final : 不可以再次重写
总结
- 纯虚函数
- 只有接口会被继承
- 必须提供实现代码
- 一般虚函数
- 接口和缺省实现代码都会被继承
- 必须继承函数接口
- 可以继承缺省实现
- 非虚继承
- 原则:绝对不要重新定义继承而来的缺省参数值
Lecture 3 多态
- 概念:同一论域中一个元素有多种解释
- 形式
- 函数重载:静态多态,不同于虚函数的动态多态
- 操作符重载
- 类属多态:template
- 函数重载:静态多态,不同于虚函数的动态多态
1.函数重载
名称同,参数不同
参数顺序,类型匹配
最佳匹配
(这个匹配每一个参数不必其他的匹配更差
这个匹配有一个参数更精确匹配)
允许窄转换(大->小,double->float)
静态绑定
2.操作符重载
动机:
- 自定义数据类型
- 提高可读性
- 提高可扩充性
操作符重载就是函数重载!!!
+重载实例:
方式1,操作符重载
1
2
3
4
5
6
7Complex operator + (Complex& x) {
Complex temp;
temp.real = real + x.real;
temp.imag = imag + x.imag;
return temp;
}
c = a.operator + (b);方式2,全局函数重载
1
2
3
4
5
6
7
8
9
10Complex operato r+ (Complex& c1 , Complex& c2 ) {
//全局函数重载至少包含一个用户自定义类型
Complex temp;
temp.real = c1.real + c2.real;
temp.imag = c1.imag + c2.imag;
return temp;
}//一般返回临时变量
Complex a(1,2),b(3,4),c;
c = a + b;//自动进行翻译
++ 自增重载实例
enum Day { SUN, MON, TUE, WED, THU, FRI, SAT}; Day& operator++(Day& d) { return d= (d==SAT)? SUN: Day(d+1); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*
* ```
class Counter {
int value;
public:
Counter() { value = 0; }
Counter& operator ++()//++a 左值
{
value ++;
return *this;
}
Counter operator ++(int)//a++ 右值,无&有哑元int
{
Counter temp = *this;
value++;
return temp;
}
}
<< 重载:实用
3. 可以重载的操作符
不可以重载的操作符:
.
(成员访问操作符)、.*
(成员指针访问运算符,如下)、::
(域操作符)、?:
(条件操作符)、sizeof
:也不重载原因:前两个为了防止类访问出现混乱,
::
后面不是变量,?:
影响理解class A { int x; public: A(int i):x(i){} void f() {} void g() {} }; void (A::*p_f)();//A类成员的函数指针 p_f= &A::f; (a.*p_f)(); int a = 0;b = 0; b?(a = 1):(b = 1);//a == b == 1 operator ?: (p,a = 1,b = 1)//均执行了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 4.基本原则
* 方式:
1. 类成员函数(类内)
2. **带有类参数**的全局函数
* 遵循原有语法
1. 单目/双目(参数个数不同):一一对应
2. 优先级
3. 结合性
## 5.双目操作符的重载
### 1. 类成员函数(双目操作符)
1. 方式1
1. 格式:`<ret type>operator #(<arg>)`
this: 隐含,必然是第一个参数
2. 使用:
a # b;//a -> this
a.operator#(b)
1 |
|
class CL {
int count;
CL(int i){…}//10可以直接隐式类型转换
public:
friend CL operator +(int i, CL& a);
friend CL operator +(CL& a, int i);
};//支持隐式类型转换就行
//如果最左边不是类对象,则必须作为友元函数
1 |
|
6.单目操作符重载
7.其他操作符
1.=号
2.[]号
3.()号
4.->号
- ->为二元运算符,重载的时候按照一元操作符重载描述。
1 |
|
- 例子:画图板程序
1 |
|
- Prevent memory Leak:需要符合compiler控制的生命周期。智能指针自动释放内存
1 |
|