OOP
WK1
1. Buzzwords
- responsibility-driven design 责任驱动设计,一个函数只做一个事情
- encapsulation 封装
- inheritance 继承
- ploymorphic method calls 多态性
- interface 接口
- iterators 枚举
- overriding 覆盖
- coupling 耦合
- cohesion 内聚
- template 模板
- collection classes 容器
- mutator methods 修改对象的方法
最上面三条是三大特性
2. Assessment
- quiz-5
- Assignment-10
- 7Labs-15
- Project-15
- Mid-Term Exam-5
- Final Exam-50
3. Introduction to C++
- C++在C的基础上
- C的知识帮助C++
- C++支持更多种类的编程
- C++提供了更多的特征
4. C++在C上的改进
- Data Abstraction
- References
- Access control
- Operator overloading
- Initialization & cleanup
- More safe and powerful memory management
- ………………
5. C++
- 面向对象
- 面向过程
6. My first C++ Program
1 |
|
7. String
要添加头文件
定义变量
1
string str;
初始化,赋值
1
string str = "Hello";
运算
1
name = str + "Mark";
成员变量
1
2
3
4
5len = name.length();
string place = "Hangzhou";
与下面一样的效果:
string place("Hangzhou");
WK2
1. 对象的指针
1.1 基础的操作
- & :获得地址
- *:获得指针所指向的对象
- -> : call the function
1.2 访问对象
1 | string s;//不初始化时是空值 |
1.3 对象的赋值
1 | string s1,s2; |
- 变量的赋值:把s2的值赋给s1
- 指针的赋值:把ps1变成ps2的地址
1.4 动态内存申请
- new 运算符,与加减乘除的地位相同
1 | new int; |
new:申请空间+初始化
malloc:申请空间
- delete:收回空间前,调用析构函数清除掉这一块空间
1 | delete p; |
Example:
1
2
3
4
5
6
7
8
9
10int *p = new int;//4bytes
int *a = new int[10];//40bytes
Student *q = new Student();//16bytes
Student *r = new Student[10];//160bytes
delete p;//4bytes被回收了
a++;//a指向了第二元素
delete [] a;
delete q;
delete r;
delete [] r;不要用delete去释放没有申请的空间
不要用delete释放两次空间
用delete [ ]如果前面用了new [ ]
用delete 如果前面申请了空间
delete空指针是可以的
2. 对象的引用
可以理解为给变量起别名,作用在引用上的所有操作事实上都是作用在该引用所绑定的对象上。
1 | char c; |
- 不能是NULL,依赖于对象
- 不能用来代表新的变量
- 不能换绑,地址不能变了
- 不能用引用去引用引用
- 不能用指针指向引用,但是可以引用指针,即原变量的地址
3. 对象与类
3.1 Point
- 在C中的结构定义
1 | typedef struct point{ |
- C++中的类的定义
1 | class Point{ |
resolver “ : :”
::
::
表示右边这个函数是左边这个class的
双冒号(::)用法(1)表示“域操作符”
例:声明了一个类A,类A里声明了一个成员函数void f(),但没有在类的声明里给出f的定义,那么在类外定义f时,
就要写成void A::f(),表示这个f()函数是类A的成员函数。(2)直接用在全局函数前,表示是全局函数
例:在VC里,你可以在调用API 函数里,在API函数名前加::(3)表示引用成员函数及变量,作用域成员运算符
4. 对象
4.1 对象的定义
- Object = Attributes属性 + Services服务
- Data数据 :the properties or status
- Operations操作/行为 :the function
4.2 对象与结构
- 结构在外面可以调用所有的东西
- class在外面可以调用public,但是如果没有说明,默认全是private
4.3 this指针
指向结构体的指针
WK3
1. 例子Ticket Machine
1.1 Object 和Class的区别
- Object是Class的实例化
- Class是一个类,类似于C中的type
1.2 OOP Characteristics
- Everything is an object
- A program is a bunch of objects telling each other what to do by sending messages.
- Each object has its own memory made up of other objects.
- Every object has a type.
- All objects of a particular type can receive the same messages.
1.3 C’tor and D’tor
1 | class Point { |
- 其中的init可以改为Point来初始化一个对象,这个叫构造函数
1 | class Point { |
默认的构造函数
是可以不用实参进行调用的构造函数- 没有带明显形参的构造函数
- 提供了默认实参的构造函数
- 若没有构造函数,则系统会构造默认的构造函数,但是这个函数不会做任何事情
- 合成默认构造函数总是不会初始化类的内置类型以及符合类型的数据成员
- 只有默认构造函数被编译器需要,系统才会合成
析构函数
当对象结束生命周期会调用析构函数- 构造函数可以有多个来构成重载,但是析构只能有一个
- 构造函数可以有参数,析构函数不能有参数
- 没有显式写出析构函数,编译器会自动加上析构函数,但是这个析构函数什么也不会做
- 析构函数的执行在return之前,return表示结束
- 指针的析构只有在delete的时候触发,return也不会触发
顺序:
继承关系的构造析构顺序
- 有类静态成员优先构造静态变量,按照定义的现后构造
- 先调用基类的构造函数
- 先构造基类的成员变量
- 基类本身的构造函数
- 派生类的构造
- 先构造派生类的成员变量
- 派生类本身的构造函数
同一级别的构造函数
类静态成员与声明的顺序无关,与继承关系也无关,根据定义的先后顺序初始化
类普通成员变量按照定义的顺序初始化,而不是构造函数里的list的顺序
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
29class B
{
private:
int val;
public:
B(int i=0){
val=i;
cout<<i<<endl;
};
~B(){};
};
class A
{
private:
B x;
B y;
//这里的定义是先X后Y
public:
A(int i=1,int j=2):y(2),x(1){};//这里的list是Y、X
~A(){};
};
int main() {
A();
}
//输出为
//1
//2
//即先初始化X再初始化Y继承关系则是根据继承的先后顺序初始化
有虚函数的先初始化虚表指针
执行初始化列表
执行构造函数
析构的顺序是构造顺序的逆序
2. Class 类的定义
2.1文件
- 在C++中有分离的.h文件和.cpp文件
- Point.h(类定义)、Point.cpp(类函数)、main.cpp(主函数)
- 成员变量是private
- 很重要的原则
WK4
1. C++Program的structure
- declaration.h //Header就是接口
- declaration_funcs.cpp(#include声明头文件)
- main.cpp
2. Standard header file structure
1 |
|
防止头文件重复添加
1 |
|
2.1 tips
- 每个头文件有一个类声明
- 在文件名的相同前缀中与一个源文件关
- 头文件的内容用#ifndef #define #endif括起来
【讨论】两个文件互相包含的情况?
答:如果A.h和B.h要互相包含,那么在其中一个.h中去掉包含加入一个临时的class,在cpp的实际使用中再包含.h
A.h
1
2
3
4
5
6
class A{
public:
B* b;
void methodA();
};A.cpp
1
2
3
4
5
void A::methodA()
{
b->methodB();
}B.h
1
2
3
4
5
6
7
8
class A; // 重点,没有包含A.h,只是声明A为一个类型,与之前声明的A没有关系
// 如果在此.h文件中使用A类型的任何属性方法都会报错
class B{
public:
A* a;//使用的本文件声明的A类型
void methodB();
};B.cpp
1
2
3
4
5
6
void B::methodB()
{
a->methodA();
}
3. Abstract
抽象是一种忽略部分细节而将注意力集中在问题的更高层次上的能力。
模块化是将一个整体划分为定义良好的部分的过程,这些部分可以分别构建和检查,并以定义良好的方式进行交互。
4. 字段fields,参数parameters,本地变量local variable
所有三种类型的变量都能够存储适合于其定义类型的值。
字段定义在构造函数和方法之外。
字段用于存储贯穿对象生命周期的数据。因此,它们保持对象的当前状态。它们的生命周期与对象的生命周期相同。
字段具有类作用域:它们的可访问性扩展到整个类,因此可以在定义它们的类的任何构造函数或方法中使用它们。
只要字段被定义为private,就不能从定义类之外的任何地方访问它们。
形式参数和局部变量仅在构造函数或方法执行期间存在。它们的生存期只有单个调用的长度,因此它们的值会在调用之间丢失。因此,它们充当临时存储位置,而不是永久存储位置。
形参定义在构造函数或方法的头文件中。它们从外部接收值,由构成构造函数或方法调用一部分的实际参数值初始化。
形式参数的作用域仅限于其定义的构造函数或方法。
局部变量定义在构造函数或方法的函数体中。它们只能在其定义的构造函数或方法的主体内进行初始化和使用。
局部变量在表达式中使用之前必须初始化——它们没有默认值。
局部变量的作用域仅限于定义它们的块。在那个块以外的任何地方都无法进入。
5. Initialization和assignment
1 | Student(string s,int a):name(s),age(a){} |
有何区别?
第一个是Initialization,在构造之前就附了初值
第二个是assignment,在构造函数内赋予初值
5.1 函数重载
在同一个作用域中可以存在多个函数名称相同但是形参列表不同的函数,这些我们成为重载函数。
【注意】:函数的参数个数和类型都相同,只是返回值不同,这不是重载函数。
【参数列表】:参数类型、参数个数、参数顺序,一个不同就是参数列表不同。
【特殊】:const关键字修饰函数可以区分重载函数,const关键字修饰参数时,要看传入的参数会不会被函数改变来决定能否区分重载
main函数不可重载
运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Point
{
int x;
int y;
public:
Point(){}
Point(int a,int b):x(a),y(b){cout<<"Ctor"<<endl;}
~Point(){cout<<"Dtor"<<endl;}
Point operator+(Point a);
void get_xy(){cout<<x<<" "<<y<<endl;}
};
Point Point::operator+(Point a){Point temp;temp.x=this->x+a.x;temp.y=this->y+a.y;return temp;}
int main(){
Point a(1,2);
Point b(4,5);
Point c = a+b;
c.get_xy();
}
5.2 默认参数缺省
缺省参数的右边一定是缺省参数
6. Container
- STL = Standard Template Library
- 容器在STL模板中
- 里面有一些数据结构Data Structure和算法Algorithm和迭代器Iterator
- 有什么:
- vector:expandable array
- deque:expandable array,expands at both ends
- list:double linked
- sets and maps
- ……
WK5
1. vector
1.1 定义
- 封装了动态大小的数组的顺序容器
- 能够存各种对象
- 可以简单地认为向量是一个能够放任意类型的动态数组
1.2 特性
- 顺序序列
- 动态数组
- 能够感知内存分配器
1.3 基本的函数
- 构造函数
1 | vector<EleType> c; |
- 基本方法
1 | V.size():容器大小 |
- 迭代器
1 | vector<EleType>::iterator I; |
- 元素访问
1 | V.at(index):访问index的数据 |
- Add/Remove/Find
1 | V.push_back(ele):在最后插入ele//调用构造函数 |
- 【习题】下列创建vector容器对象的方法中,错误的是。
- A.vector
v(10); - B.vector
v(10, 1); - C.vector
v{10, 1}; - D.vector
v = (10, 1); - 答:D,A指的是创建大小为10的int类型的vector;B指的是创建大小为10的int类型的vector并且值全部为1;C指的是创建vector,里面内容有10,1,size为2.
2. list链表
2.1 定义
- 相当于C的链表
- Constructors
- x.front( ) 和 x.back( alt=”image-20220325102200722” style=”zoom:50%;” />
3. map
3.1 定义
- 多个对,相当于python的字典
- 有key和value
- 查找使用key查找,找回一个value
- example如电话本
- 里面的二元组是按照key的顺序排列的。
3.2 操作
构造
1
map<ktpye,vtype> map_name; 可以构建两个type为一组的map
添加数据
1
2
3
41. Insert法
map_name.insert(pair<ktype,vtype>(key,value));
2. 数组法//用这个比较好
map_name[key]=value;遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
181. 前向迭代器
map<ktype,vtype>::iterator it_name;
for(it_name=map_name.begin();it_name!=map_name.end();it_name++){
cout<<"key:"<<it_name->first<<" value:"<<it_name->second<<endl;
}
注意,这里的first和second分别对应了key和value
2. 反向迭代器
map<ktype,vtype>::iterator it_name;
for(it_name=map_name.rbegin();it_name!=map_name.rend();it_name++){
cout<<"key:"<<it_name->first<<" value:"<<it_name->second<<endl;
}
3. 数组形式
int size = map_name.size();
for(int i=1;i<=size;i++){
cout<<map_name[i]<<endl;
}查找
1
2
3map<ktype,vtype>::iterator it_name;
it_name= map_name.find(key);
若返回为end则没找到删除
1
2
3
4iterator erase(iterator it) ;//通过一个条目对象删除注意输入的是迭代器
iterator erase(iterator first,iterator last); //删除一个范围
size_type erase(const Key&key); //通过关键字删除
clear();//就相当于enumMap.erase(enumMap.begin(),enumMap.end());sort问题
Map 中的元素是自动按 key 升序排序,所以不能对 map 用 sort 函数。
Tips:
嵌套使用时注意尖括号之间的空格,否则可能会被视作右移>>
【迭代器】
按照定义分为:正向、反向、常量正向、常量反向
按照功能分为:输入、输出、正向、双向、随机访问五种
4. Function
4.1生存期和作用域
- 成员变量:作用域为类内部,生存期为跟随对象
- 全局变量:作用域为全部文件,生存期为长久的
- 局部变量:作用域为局部变量所在函数内,生存期为创建开始到函数调用完毕
4.2 生成函数
- 先进行初始化再进行生成函数,即private先赋值,但是会被生成函数覆盖。
4.3 重载overload
- 在同一个作用域内,可以生命几个功能类似的同名含糊,但是形式必须不同,如参数个数、类型或者、顺序
- 返回类型可以不同也可以相同
- 函数的重载仅仅是语法层面的,本质是不同的函数
- 重载的实现:C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)会被重命名为_Swap_int_int,void Swap(float x, float y)会被重命名为_Swap_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。
4.4 默认函数default argument
- 在函数里加一个默认值,调用时可以不写,会用这个默认值替代
- 要从右往左写
- 在声明里面可以写
- 在定义里默认值是不能写的
5. Friends
- 私有的成员可以授权给其他(朋友)使用
- 声明friend要在本体类中声明,不能在类生成后再添加friend
- 子类的友元不能访问父类的保护成员。
- 基类的友元函数在派生类也是无效的,但这时可以将派生类转换成基类操作,是可以的。
6. class和struct的区别
- class默认为private的
- struct是public的
7. Function的Way in
1 | void f(Student i); |
8. Function的Way out
1 | Student f(); |
【错例】:这里函数中返回了一个局部变量的指针,这个局部变量在函数return时被释放,那么返回的A*就是错误的了。
1 | A* f(){ |
【tips】
- Pass in an object if you want to store it
- Pass in a const pointer or reference if you want to get the values
- Pass in a pointer or reference if you want to do something to it
- Pass out an object if you create it in the function
- Pass out pointer or reference of the passed in only
- Never new something and return the pointer
WK6
1. 内联函数Inline Function
- 内联函数,要放在最前面
- 内联函数在编译时是将该函数的目标代码插入每个调用该函数的地方
- 声明原型和定义函数时都要加inline声明,但其实并不会分开写
- 定义声明body要在调用之前,整个inline函数在头文件里或者只在一个文件里。
- 在类里面写,不能把inline放在cpp里,他是一个声明,要放在.h头文件里面
- 去掉了函数调用和返回的开销,但是增加了可执行程序的大小,用空间换时间
- 比C语言的宏要好,宏是字符替换,inline仍是函数
- 编译器会觉得你的函数太复杂了会把inline变成普通函数,觉得你的普通函数太简单了会变成inline,这都是编译器决定的
1.1 什么时候用Inline?
- small function
- Frequently called functions
1.2 什么时候不用Inline?
- Vary large function
- Recursive function
1.3 A lazy way
- 所有的都是inline
- 所有都不是inline
2. Const
2.1 定义:
- 常数是一个变量
- 一种不变的变量
- 有变量空间,编译器在运行时可以改,但是代码里不能改
- C++中默认为内部连接
- 可以使用const进行聚合,但会分配存储空间。 在这些情况下,const表示“不能更改的存储空间”。 但是,不能在编译时使用该值,因为编译器在编译时不需要知道存储的内容。
2.2 指针的const
如何理解?到过来读,“ * ”读作“is a pointer to”。
1 | const string *p = &p1;//p is a pointer to const string |
2.3 对象中的const
2.3.1 成员函数的const
- const对象不可以引用非const成员阐述,可能调用const成员函数
- 非const对象可以调用一切成员函数
- const的成员函数写法在后面加上const
- const成员需要默认构造,或者说要保证有初始值!!!
- const成员函数不可以改变非multable数据的值
- 非const对象可以调用const也可以调用非const成员函数
- const是可以用来区分重载的!
1 | //常用指针传入对象 |
2.3.2 成员变量的const
第一次要获得初始值,但是在运行时刻不能改变。
1 | class A{ |
2.4 tips
- const的值必须初始化
- 除非使用extern声明
- 使用了extern 声明
2.5 运行时的常数
1 | const int class_size = 12; |
2.6 const的聚合
可以聚合使用const,但是会视作开了一块内存,并且“内存不能被改变”,但是编译时不能使用这块内存中的值,因为编译器在编译时不知道这里的值。//有的编译器支持
1 | const int i[]={1,2,3}; |
2.7 Const Object
不能改变const对象的属性值
因此使用const函数:不会改变对象的属性值的函数可以在声明和定义后加上const,以供const对象使用
并且Const对象只能使用const函数
3. static
静态本地变量和静态全局变量不是同一个static
想让某一变量可以在下一次函数调用使用,则使用static关键字
1 | int f(){ |
控制变量的储存方式和可见性
在修饰变量的时候,static修饰的静态本地变量只执行一次,延长了局部变量的生命周期,直到程序运行完后才释放
构造函数的调用是在执行时才调用的,仅仅执行一次
static修饰全局变量时,全局变量只能在本文件中使用,不能再其他文件中访问,多文件的初始化顺序是不能确定的
static修饰一个函数,则函数只能在本文件中使用,不能被其他文件调用
类的静态成员函数:
- 静态成员函数是类的一个特殊的成员函数
- 静态成员函数属于整个类所有,没有this指针
- 静态成员函数只能直接访问静态成员变量和静态成员函数
- 可以通过类名直接访问类的公有静态成员函数
- <class name>::<static member>
- <object name>.<static member>
- 可以通过对象名访问类的公有静态成员函数
- 定义静态成员函数,直接使用static关键字修饰即可
类的静态成员变量:
- 访问受限于类内部
- 静态成员变量要类外分配内存空间
- 静态成员变量属于整个类所有
- 静态成员变量的生命期不依赖于任何对象,为程序的生命周期
- 可以通过类名直接访问公有静态成员变量
- <class name>::<static member>
- <object name>.<static member>
- 所有对象共享类的静态成员变量
- 可以通过对象名访问公有静态成员变量
- 静态成员变量在程序内部位于全局数据区 (Type className::VarName = value)
1 |
|
静态成员变量需要在cpp文件里定义全局变量,不能加static
1
2
3
4
5
6//my.h
class StatMem{
public:
int m_b;
}
extern int max(int a,int b);1
2
3
4
5
6
7
8//my.cpp
using namespace std;
int StatMem::m_b;
int max(int a,int b){
return (a<b)?b:a;
}1
2
3
4
5
6
7//main.cpp
int main(){
int x=0;
int y=1;
int z=max(x,y);
}
WK7
1.1 namespace
- namespace的产生
1 | //old1.h |
- 为了防止混淆,放入命名空间,一个f属于old1的namespace,一个f属于old2的namespace。
1 | namespace Math{ |
定义namespace要放在头文件,在namespace里面,变量是定义,不是声明,函数是声明,不是定义。
直接使用函数的名字可以多个一起用,如:
//MyLib.cpp文件 #include<iostream> #include"MyLib.h"//包含名字空间生命所在的文件 using std::cout;//这是使用生命,不是使用指令 using std::endl; int MyLib::i=10;//这是变量i的定义,并且初始化,当然也可以不用初始化直接写int MyLib::i; void MyLib::fun(){ cout<<i<<endl; }
1
2
3
4
5
6
7
8
9
- 当使用多个namspace时,要确保缺省后的函数不会重复,否则需要使用名字空间
- ```cpp
using namespace name1;
using namespace name2;
name1::test(); // 不同命名空间中的相同的函数名,通过前面的命名空间来区分。
name2::test();可以使用conbine将多个namespace合起来
```cpp
namespace mine {
using namespace first;
using namespace second;
using first::y(); // resolve clashes to first::x()
void mystuff(); …}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
### 2. 继承
#### 2.1 重用
<img src = "OOP笔记\image-20220408101603737.png">
- 继承是一种重要的设计方法,在类与类之间**共享设计**,而非数据
- 继承后的类往往比被继承的类有更多的属性
- <img src = "OOP笔记\image-20220408101816418.png">
- **继承可以表达为 A is B**,student is a person
<img src = "OOP笔记\image-20220408104701455.png">
- 继承可以消除代码的复制,易于维护,有拓展性,拿已有类表示新类,新类除了已有类的特征还有自己的特征
#### 2.2 Example
```cpp
class Employee {
public:
Employee( const std::string& name,const std::string& ssn );
const std::string& get_name() const;
void print(std::ostream& out) const;
void print(std::ostream& out, const std::string& msg) const;
protected:
std::string m_name;
std::string m_ssn; };
Employee::Employee( const string& name,const string& ssn )
: m_name(name), m_ssn( ssn)
{
// initializer list sets up the values!
}
inline const std::string& Employee::get_name() const {
return m_name;
}
inline void Employee::print( std::ostream& out )const {
out << m_name << endl;
out << m_ssn << endl;
}
inline void Employee::print(std::ostream& out, const std::string& msg) const
{
out << msg << endl;
print(out);
}
///now add Manager!
class Manager : public Employee {
public:
Manager(const std::string& name,const std::string& ssn,const std::string& title);
const std::string title_name() const;
const std::string& get_title() const;
void print(std::ostream& out) const;
private:
std::string m_title;
};
2.3 构造函数的继承
1 | class A{ |
- 先进行父类的构造,再进行子类的构造,同名的属性会覆盖
- 在子类中如果有对象,则先进行基类的构造,再进行对象的构造
- 子类的对象由两部分组成:父类和子类的特有
2.4 public,private,protected继承
继承方式 | 基类public | 基类protected | 基类private |
---|---|---|---|
public继承 | public | protected | 不可访问 |
protected继承 | protected | protected | 不可访问 |
private继承 | private | private | 不可访问 |
WK8
1. 多态
- 如果B继承自A,那么在使用A的地方都可以使用B,因为B继承自A
- 替换无效请小心
- 在继承时可能会有多态的情况,即一个父类变成了各式的子类,查看这个例子
1 |
|
这个例子中输出为
1 | Parent class area : |
因为调用函数area()时被编译器设置为基类的版本,即静态绑定,函数调用在程序执行前就准备好了,就是说area( )函数在程序编译期间就设置好了。但是我们想要的是不同的子类的area函数不同,那么我们要在基类里使用virtual关键字
1.1 Up-casting
一个继承类有一个基类,继承类由基类的一切属性,也是基类的一员!
It is to say: Students are human beings. You are students. So you are human being
1.2 virtual 关键字
父类有一个virtual关键字的函数则,说明子类也会有这个同名函数要进行覆盖.
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
1 | class A{ |
- 虚函数:被virtual关键字修饰的成员函数,就是虚函数(实现多态)
- 限制:
- 非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。一般我们把析构函数写成虚函数。
- 只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
- 当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数。
- 如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种同名函数。
- 限制:
1.3 A drawing program
Rectangle、circle、Ellipse
操作:
- 渲染
- 移动
- 调整
数据:
- 中心
子父类的选择:
shape→Ellipse→circle
圆是特殊的椭圆
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class XYPos{...};
class Shape{
public:
Shape();
virtual ~Shape();
virtual void render();
void move(const XYPos&);
virtual void resize();
protected:
XYPos center;
};
class Ellipse:public Shape{
public:
Ellipse(float maj,float minr);
virtual void render();
protected:
float major_axis,minor_axis;
};
class Circle : public Ellipse {
public:
Circle(float radius) :
Ellipse(radius, radius){}
virtual void render();
};
1.4 邦定binding
绑定(Binding)是指将变量和函数名转换为地址的过程。
https://zhuanlan.zhihu.com/p/192178632
- 静态类型:对象在声明时采用的类型,在编译期既已确定;
- 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
- 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
- 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
静态绑定:
call the function as the code
动态绑定
call the function of the object
从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。
在构造函数中调用虚函数,不是动态联编。
1.5 Virtual destructors 虚的析构函数
1 | Shape *p = new Ellipse(100.0F,200.0F); |
- 除非一个类以后无法被继承,否则这个类的析构要是virtual的
1.6 Overriding
- override保留字表示当前函数重写了基类的虚函数。
- 名称相同,参数表相同,父类是virtual,子类无所谓,最好加上,因为子类可能还要被继承
- 返回类型也要求相同,但是这个相同可以是继承关系
- 只有虚函数能够重载(参数列表、函数名称、const)
1.7 Virtual in Ctor
1 | class A { |
说明函数声明在构造之前。
1.8 Abstract base classes 抽象类
一个有纯虚函数的类是抽象类
抽象类不能制造出对象(不能被实例化)
但是可以有指针,只是没有实例化
纯虚函数是把一个函数=0
只定义了接口,没有function body
为什么用抽象类
- 建模
- 强制正确行为
- 定义了接口没有定义实现
什么时候用
- 没有足够的信息
- 设计接口继承等
1.9 接口类Protocol / Interface classes
接口类:
只提供接口不提供实现的类,接口类和抽象类对C++而言没有区别。
- 子类实现接口类中的所有接口
- 接口方法前有virtual修饰且等于0,全是纯虚函数
- 只能被继承不能生成对象
与抽象类的区别
- 接口里只能包含抽象方法,静态方法和默认方法(加default),不能为普通方法提供方法实现,抽象类则完全可以包含普通方法,接口中的普通方法默认为抽象方法。
- 接口不能包含构造器,抽象类可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 不能包含初始化块接口里不能包含初始化块,但抽象类里完全可以包含初始化块。
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
1 | class BaseInterface{ |
1.10 多类继承
1 | class Employee { |
此时Consultant有MTS和Temporary的属性
【注意】虚表
每个包含虚函数的类都有一个虚表,虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。为了指定对象的虚表,对象内部包含指向一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr
,用来指向虚表。 这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
【注意】类的sizeof
- 空类的大小:1
- 一般非空类的大小:成员变量sizeof的总和
- 有虚函数类:在成员变量的总和的基础上加上指向虚表的指针(4)
- 有继承的类:基类加子类大小
【习题】
Given:
1
2
3
4
5class A {
A() {};
virtual f() {};
int i;
};which statement is NOT true:
A.i is private
B.f() is an inline function
C.i is a member of class A
D.sizeof(A) == sizeof(int)
答:BD均错。D中的sizeof(A)=4+4=8
WK9
1. Copy Ctor 拷贝构造
复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。
1 | void f(){ |
这是函数原型声明,students( )函数返回一个Stash
1.1 Copying
1 | class A{ |
拷贝构造:
如果没有拷贝构造函数,编译器会自动插入拷贝构造函数
参数是本类的一个对象的引用
1
2T::T(const T&);//要有const
T::T(T&);//不安全没有拷贝构造:编译器会给你一个拷贝构造,自己写的拷贝构造要把所有的属性拷贝过来。必须要做拷贝构造的情况:成员里有指针,或者有些属性不想被拷贝过来。
何时被调用?
- 函数使用这个对象时,不是使用指针
- 用另一个对象初始化时
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
30class A{
public:
int i;
public:
A(int ii=0):i(ii) { cout << "Actor"<<endl; }
A(const A& a) {
i = a.i;
cout << "Copy"<<endl;;
}
void print() const { cout << 3 << i; }
};
void f(A aa){cout<<aa.i<<" f"<<endl;}
void g(A* aa){cout<<&aa->i<<" g"<<endl;}
int main()
{
A a(1);
A b=a;
A c(2);
c=a;
f(a);
g(&a);
}
/*
Actor//默认构造函数
Copy//拷贝构造在在赋值使用,b默认构造没有调用
Actor//c默认构造,没有使用拷贝构造
Copy//函数调用的拷贝构造
1 f//f
0x78fe14 g//g是使用指针传入的没有调用拷贝构造
*/有指针的到时候要自己写拷贝构造
WK10
1. 复制构造什么时候调用
调用函数时,函数的参数是对象本身,不是指针不是引用
1
A f(A aa){};
拷贝赋值,赋值对象没有定义时
1
2A a(10);
A b=a;拷贝构造
1
2A a(10);
A b(a);函数返回时,返回值可能调用拷贝构造,若返回了值被赋值到新的对象上,则需要拷贝构造。
1
2
3
4
5
6A f(){
A aa(20);
return aa;//copy
};
A b=f();//copy
//有的编译器会把这两个拷贝构造优化
1.1 拷贝构造和赋值
- 对象的构造只能构造一次
- 当一个对象被构造,他能作为一个变量来赋值,C++的特性
2. 运算符重载
加减乘除的运算可以进行重载,可以自己写一个类重写运算符。
2.1 限制
- 只有已经存在的运算符才能重载
- 不能重载的有
.
,.*
,::
,?:
,static_cast,dynamic_cast,const_cast,reinterpret_cast - 只能在类和一个枚举type中重载,不能对cpp原来自己的类的运算符重载。
- 运算符的操作数的数量、优先级、结合律等要一致
2.2 如何写?
operator关键字
1 | const Mystring Mystring::operator +(const Mystring& that)//类函数 |
- 一元操作符要是成员函数
=
,()
,[]
,->
,->*
必须是成员的- assignment operators should be members
- All other binary operators as non-members
类内定义:
1 | class A{ |
全局定义
1 | class A{ |
- 在全局的更好!
- z=x+y
- z=x+3
- z=3+x;
- z=3+5 //z=8是一个构造!!!
2.3 成员or全局?
- Unary操作符要是成员的,= () [] -> ->*要成员的
- 一般在类内会使用友元函数定义
- 参数类型与返回类型,参数类型视操作数是否改变而定,返回类型视对象是否做左值,是否要做操作。
2.4 ++操作符和–操作符
a++和++a
1
2
3
4
5
6
7
8
9const Integer& Integer::operator++(){
*this += 1;
return *this;
}//++a;
const Integer Integer::operator++(int ){//做了一个int的标识
Integer old(*this);
++(*this);
return old;
}//a++;
2.5 关系运算符
返回bool值
2.6 [ ]操作运算符
返回是一个reference
```cpp
class A{
public:
int i;
public:
A(int ii=0):i(ii) { cout << “Actor”<<endl; }
A(const A& a) {
i = a.i;
cout << “Copy”<<endl;;
}
void print() const { cout <<i<<endl; }
int geti(){return i;}
int& operator[](int idx){
return i+idx
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## WK11
### 1. Stream Extractor(>>) and Inserter(<<)
即<<和>>的重载
#### 1.1 Extractor
**这个函数一定是全局函数**,因为一定是有跟着cin和cout的,**不一定是iostream的friend**,但是一定要是你写的类的友元函数,返回一个**istream的reference**
```cpp
istream& operator>>(istream& is,T& obj){
//your code
return is
}
1.2 Inserter
返回类型是ostream的引用,注意这里的对象T应该是要const,因为我们只输出的话不会更改T,因此,我们要这么写:
1 | ostream& operator<<(ostream& os,const T& obj){ |
1.3 endl是什么
是一个manipulator,实质是一个函数,但是不用括号如:
1 | ostream& manip(ostream& out) |
2. Copying vs Initialization
1 | MyType b; |
3. 赋值运算符的重载
赋值运算符必须是一个Reference,因为要做左值,赋值是赋值给自己,因此我们要判断赋值值和赋值对象的值要判断是否一致,不能自己给自己赋值
1 | T& T::operator=(const T& rhs){ |
- 注意:如果有数组之类的new出来的空间在new之前要delete
- this和&rhs的指针比较,this和rhs的数值比较哪个好?*地址比较更好。
- 对于具有动态分配内存的类,声明赋值操作符(和复制构造函数)
- 为了防止赋值,显式地将operator=声明为private
4. Value classes
表达一些值的类,这些值要在函数间传入传出
- 定义类型转换
- 需要overload operator等
4.1 类型转换
1 | class PathName{ |
建议不要重载=号来实现不同的类型转换,因为赋值一般是一样的类型的,而且需要转化的类有很多不可能写专门的类。
explicit关键字说明这里只用于显式调用构造。
class A{ public: int i; public: A(int ii=0):i(ii) { cout << "Actor: "<<ii<<endl; } A(const A& a) { i = a.i; cout << "Copy"<<endl;; } const A operator++(){ this->i +=1; return *this; } const A operator++(int){ A old(*this); ++(*this); return old; } void print() const { cout <<i<<endl; } void print_nonc(){cout<<"nonconst "<<i<<endl;} int geti(){return i;} }; const A operator+(const A &x,const A &y){ cout<<"-outside"<<endl; int ret=x.i+y.i; return A(ret); } int main(){ A a(1); A b(2); A m1=a+3;//若构造函数前没有explicit则正确,否则不可隐性构造导致错误。 k.print(); a.print(); }
- 操作符名称是任何类型描述符 - 无显式参数 - **没有返回类型** - 编译器将它用作从X的类型转换T1
2
3
4
5
- 类型转换函数的一般形式:
```cpp
X::operator T()用户定义的T→C
- C(T),C从T构造而来
- T::operator C( ),重载了C( )操作符
- 但是两个方法不能同时使用,在构造时没问题,但是在等号赋值时,编译器不知道是构造C,从T到C的方法两种不知道用那种。
- 要解决可以在构造函数上加上Explicit
Overload遇上了转换:成本最低原则,匹配最好的类型
5. 对象初始化
1 | //小括号初始化 |
5.1 列表初始化
1 | class Test |
5.2 容器初始化
1 | vector<string> vs={ "first", "second", "third"}; |
6. 函数的参数和返回值
6.1 Way in
void f(Student i);
- a new object is to be created in f //在f中要拷贝构造
void f(Student *p);
- better with const if no intend to modify the object
void f(Student& i);
- better with const if no intend to modify the object
6.2 Way out
- Student f();
- a new object is to be created at returning
- 新的obj会返回出来
- Student* f();
- what should it points to?
- Student& f();
- what should it refers to?
6.3 Tips
- 希望存储对象,传入对象而非指针和引用
- 因为要复制一份存下来,不能复制地址
- 只想要传入值,只需要const pointer 或者 reference
- 如果创建了对象,传出对象
- Pass out pointer or reference of the passed in only
- Never new something and return the pointer
7. 左值和右值
可以简单地认为能出现在赋值号左边的都是左值:
- 变量本身、引用
- *、[]运算的结果
只能出现在赋值号右边的都是右值
- 字面量
- 表达式
引用只能接受左值
- 引用是左值的别名
调用函数时的传参相当于参数变量在调用时的初始化
7.1 右值的引用
1 | int x=20;//左值 |
右值引用变量的初始化只能是右值
但是 alt=”image-20220622201829652” style=”zoom:50%;” />
8. 移动构造
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。
1 |
|
WK12
1. 模板Template
1.1 什么是模板
假设我们需要一个list容器,想要有放多种类型的容器,这些代码是非常相似的,唯一的不同是类型不同。
可以使用公共父类
可能不太满足我们的要求
clone code
难以管理
Untyped lists 无类型的list
type unsafe
元代码:
- 泛型编程
- 使用类型作为参数在类或函数定义
函数模板
类模板
1.2 函数模板
1 | template <class T> //必须单独一行 |
- 调用时优先调用非模板函数
- 当函数中没有使用T时,一定要加上
来区别
1.3 类模板
1 | template <class T> |
- 类模板:类的声明、类的成员的定义
- 注意成员的定义:要template,
等
1.4 Tips
- 模板不被编译,模板是生成代码的
- 模板也支持多个类型的定义
- 如:template <class key,class value>
- 注意:<Vector Vector<double *> >这里加个空格
- 模板也支持中间有固定类型与缺省参数
- 如:template<class a,int num=100>
- 类模板记得静态变量需要一个全局变量
- 但是类模板的全局变量不能是类模板本身的一个对象
1 | template <class T> |
Templates的继承
普通类可以派生类模板
- ```cpp
templateclass Derived : public Base { } 1
2
3
4
5
- 能继承类模板
- ```cpp
template <class A> class Derived : public List<A> { }
- ```cpp
非模板类可以继承模板类
- ```cpp
class SupervisorGroup : public List<Employee*> { }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 类模板中同样可以定义友元函数
```cpp
template<T>
class A{
public:
A(const T& ii):i(ii){};
void f(){cout<<kk<<endl;}
friend void g(){};
private:
T i;
static T kk;//最好不要这么写
};
void g(){cout<<i<<endl;}
template <class T>
int A<T>::kk=0;///Error
int main(){
A<int> a(0);
}
- ```cpp
WK13
1. Exceptions 异常处理
程序运行的时候处理发生的情况,不是error的而是一种solution,无法避开。
能够与之发生,异常不一定发生,但是一定 会 发生
读文件、打开文件、判断文件大小、申请内存空间、关闭文件。
每一个步骤都可能存在异常,一旦异常,后续无法继续进行。
1.1 异常处理的语法
try( ) catch{ }
;- 表示try中的为保护代码,try中出现了错误,错误发生后的代码不会被执行,而是直接跳转到对应的catch中,处理完后继续向下运行
- throw语句:抛出异常
throw exp
抛出一个表达式,throw的参数可以是任何的表达式,表达式中的类型决定了抛出结果的类型throw
把原本捕捉的异常抛出,只在catch子句中有效- throw下方的语句都不会被执行,离开当前语句
1.2 异常处理的过程
程序按照正常的顺序执行,执行到try,开始执行try内的保护段
如果在保护段执行期间没有发生异常,那么跳过所有的catch
如果保护段的执行期间有调用的任何函数中有异常,则可以通过throw创建一个异常对象并抛出,程序转到对应的catch处理段
首先要按顺序寻找匹配的catch处理器,如果没有找到,则 terminate( ) 会被自动调用,该函数会调用abort终止程序
- 如果在函数中进行异常处理并且触发了terminate,那么终止的是当前函数
- 异常类型需要严格的匹配
如果找到了匹配的catch处理程序,并且通过值进行捕获,则其形参通过拷贝异常对象进行初始化,在形参被初始化之后,展开栈的过程开始,开始对对应的try块中,从开始到异常丢弃地点之间创建的所有局部对象的析构
1.3 Try block
1 | try{ |
- 异常类可以继承的,抛出了A类异常,B为A的父类,接受B类的catch是可以接受A类的。如果既有捕捉A的又有捕捉B的,优先第一个捕捉的catch,其他catch不会再看了
1.4 new的exception
- new does NOT returned 0 on failure
- new raises a bad_alloc() exception
【C++中自带的异常的继承体系,定义在头文件 <exception> 中】
what方法给出了产生异常的原因,是异常类之间都有的公共方法,已经被所有的子异常类重载
自定义的异常类:需要继承exception类
载
1.5 函数后面加throw()
- 可以在函数名后面加 noexcept 关键字,说明该函数在运行的过程中不抛出任何异常,如果
还是产生了异常,就会调用std::terminate
终止程序 - 可以在函数声明中列出所有可能抛出的异常类型,比如
double f(int, int) throw(int)
; - 如果是
throw()
表示不抛出异常,就算函数里有throw也不会执行 - 如果是
throw(...)
表示抛出所有形式的异常 - 没有
throw()
则任意异常抛出
https://blog.csdn.net/to_baidu/article/details/53763683
1 | class T |
WK14
1. An Example
Templates、Inheritance、Reference Counting、Small Pointers
1.1 Goal
C++没有实现自动内存管理的
引用计数:
对象在堆里有一个指针,当最后一个指针不指它,引用计数为0时析构
```cpp
a = new A();//Aa的计数=1
b = a;//Aa的计数=2
b = new A();//Aa计数=1,Ab=1
a = b;//Aa的计数=0,析构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 引用计数的变化:在赋值时变化,左边的指针-1,右边的指针+1。
- Class UCObject
- 这个类是计数的
- UCPointer指向一个UCObject,是一个模板
#### 1.2 String类
这是我们想要实现的自定义字符串类
<img src = "OOP笔记\image-20220527100711038.png">
Shared Memory:我们要自动回收这个abc内部的数据的自动回收
```cpp
String abc("abcdef");//会被转成const char*
String def = abc;//Copy Ctor
1 | abc = "Hello world";//copy on write |
Reference counting
1 | //当 |
Reusing Reference Counting
1 |
|
UCPointer
1 | template<class T> |
String Rep
1 |
|
String
1 |
|
WK15
Stream
输入输出流,原始的C的IO使用printf和scanf,stream在C++中使用,但是C的IO仍可使用。
Stream的优点:type safety、Extensible、More object-oriented。
Stream的缺点:比较啰嗦、比较慢
1.1 C++ VS C
可以只使用C也可以使用C++,但是不能混用
1.2 Stream
什么是流:
- Common logical interface to a device
- Sequential
- Can:produce values、consume values
流的分类:
- Text streams
- Deal in ASCII text
- Perform some character translation
- Include
- Binary streams
- Binary data
- No translations
1.3 Predefine streams
- cin:standard input
- cout:standard output
- cerr:unbuffered error(debugging)output
- 需要定向到一个地方,在程序的输入输出之外有一个其他地方输出可以看内部的东西
- clog:buffered error(debugging)output
1.4 Other input operators
int get( )
Returns the next character in the stream
Returns EOF if no characters left
Example: copy input to output
istream & get (char& ch)这里有隐藏的参数this
1
cin>>get(&c);
get( )不读回车
1.5 Formatting
头文件:iomanipint n;
1 | cout << "enter number in hexadecimal" << flush; |
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !