1.学习良好的编写C++类的形式,包括两种,分别是无指针成员类(如Complex)和有指针成员类(如String)
2.学习类之间的关系,即继承,复合和委托。
C中存在数据以及函数,函数用来处理数据。缺点是缺少关键字管理数据,数据均为全局,难以作限制
C++中通过类将数据和函数包在一起,以类为个体来创建对象。是面向对象的语言。
#ifndef _name_
#define _name_
//本体
#endif
优点:防止多次引入同一个文件
因为可能存在不同的参数类型,所以通过template可以简化很多编写重载操作
在class内部定义的函数,自动声明为inline,在外部生命的函数在前面加上inline也可以手动声明为inline。不过具体能不能成为inline函数要看编译器。
inline的优点是相对于普通函数更快,一般来说能声明为inline都声明为inline。
函数一般放在public,数据一般放在private,如果要访问数据,可以在public写单独的获取数据的函数以访问数据。
使用初始化队列初始化和在{}使用赋值语句(如下图)的区别:
不一样,前者是在初始化,第二个是在赋值,前者更加规范,而且效率更高,也会避免一些错误。比如说有些必须初始化的元素(比如引用),使用赋值可能留下隐患(引用必须在声明时初始化)。
1.函数名称。函数重载需要函数名称相同
2.函数参数。函数重载需要不同的函数参数列表,列表可以是类型不同,数量不同等
1.如果只有返回值类型不同则不会构成重载
2.对于每种参数,必须要有优先级区分,不然会造成二义性调用。
说明外界不能调用来创建这个类的对象。
如果是这样的形式,那么这个类可能会是一个单例,即这个类仅允许存在一个它的对象。
如果这个函数内部不会改变数据,那么这个函数应当声明为const。比如下面两个函数仅仅是读取放在private区域的值,就声明为const
如果不声明为const,那么遇到下面这种情况,即声明了这个类的一个const对象时,如果调用了非const的函数,那么就会报错,即留下了隐患。
如果进行值传递,那么函数会将这个需要传的值本身全部压栈,如果这个值本身很大,那么必然会花费不少时间和空间。
然而,如果进行引用传递,那么不论这个传递的值本身多大,函数也只会传递一个指针大小的值过去(引用实际上就是使用指针实现的),即使这个值本身很大,它的时间和空间复杂度也没有变化。
所以我们需要养成习惯,如果可以的话,尽量使用引用传递
因为对引用进行修改会对初始值也造成修改,如果我们不需要对传入值进行修改的话,应该添加const。
如果确定要对引用传递进行修改,切记它也会对原本的对象进行修改。
如图,c2可以直接访问c1(即直接通过点运算符)的成员,而不是通过函数返回值获取成员
如果返回值是临时创建在函数内部的话,因为它的生命会在函数运行结束之后终止并死亡,所以如果对一个被销毁的对象传引用的话,那么必然会发生错误。
如果调用这个函数重载,那么c2将作为参数this,c1将作为参数r
如果不进行连续赋值,那么确实可以不用返回引用,返回void即可。但是比如说连续赋值如下:
我们希望它应该计算c2+=c1,然后将之后得到的c2进行c3+=c2的操作。即c3 = c3 + c2 + c1。
如果我们没有使用引用传递,那么最后的一开始计算c2+=c1返回的就是一个void,c3将与一个void相加,必然会报错。
这就是我们返回引用的理由
<<运算符会将右边的对象作用于左边的对象身上。最常见的就是
cout<<123;//在屏幕中打印123
这里ostream不能设置为const,因为os一直在接受输入,即一直在改变,所以不能设置为const。
然后,由于我们经常这样使用
cout<< 1 << 2 << 3; //连续使用<<输出打印
我们需要设定它的输出为ostream&,以便于连续使用。
1.使用防卫式声明
2.将函数放在public,数据放在private
3.在构造函数中,使用初始化列表来对数据进行初始化
4.尽量使用引用传递来传递参数和返回值
5.如果函数内不会对数据进行修改,那么应该声明为const
6.如果有需要,记得自己再重载<<操作符实现输出
因为在设计类的时候,我们无法确定使用者需要多大的内存空间来存储这个字符。所以在设计这个类的时候,设计者选择在类中存放一个指针,然后创建对象时再动态的在内存空间当中分配足够的空间来容纳这个字符串,并且让类中存放的这个指针指向刚刚分配的内存空间的地址。
假设我们需要实现以下的功能
int main(){
String s1();
String s2("Hello!");
String s3(s1); //拷贝构造
cout << s3 << endl; //重载<<运算符
s3 = s2;//拷贝赋值
cout << s3 << endl;
}
public区,从上到下依次是:
1.构造函数
2.拷贝构造函数
3.拷贝赋值函数
4.析构函数
5.普通函数,用来读取数据
private区,存放一个指针,用来动态指向分配的内存地址。
已知c风格字符串需要以’\0’结尾
strlen()函数不会获取后面的’\0’,所以需要额外+1
strcpy()函数是将我们接收到的cstr放到我们刚刚动态分配的内存空间中去,这样,我们获得了独立的一份字符串拷贝。
因为类创造的对象死亡时,会自动清理它的内部所有的成员。对于类似Complex的类来说,不需要额外编写析构函数。但是对于我们在类内部动态创建内存空间的String类来说,它只会清理它内部的指针,而不会清理指针所指向的那一份内存空间。
所以我们需要额外编写析构函数,目的是是用来释放我们刚刚自己动态分配的内存空间,避免内存泄露。
析构函数的形式如下:
~ 类名(){
//删除语句
}
如果使用默认的拷贝构造(copy ctor)和拷贝赋值(op=)函数,那么不仅a和b会指向同一个对象,对a和b其中的任意一个进行修改都会影响到第二个;同时也发生了内存泄露,因为它原本指向的内存空间也没有得到释放。
这样的拷贝也叫做浅拷贝。
↑↑↑↑↑↑↑↑↑↑↑↑默认的赋值函数↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
这里可以知道,如果使用默认的拷贝构造函数,不仅会是
那么我们如果不想让a和b指向同一个对象,二是互相独立,我们就需要自己额外编写拷贝构造函数(copy ctor)。
这也叫深拷贝,因为它自己创建了一个新的对象
(这里也应用到了之前提到的性质,就是同一个类初始化出的对象互为友元)
如果修改一个原本已经存在的值,我们需要做的有三点,
一是删除原来的值,清除其原本所指的内存空间
二是创建一个新的大小合适的内存空间来容纳新的数值
三是将新的数值拷贝到这个新的内存空间中去
即下图中1,2,3所示操作
一是删除原来的值,清除其原本所指的内存空间
同时这里注意一下,返回的是引用,允许了我们进行连续=操作(s1 = s2 = s3)
ostream& operator<<(ostream& os, const String& str){
os << str.get_c_str();
return os;
}
这个重载应当设置为全局函数,不然<<将出现在右边,不符合我们的习惯
第一步实际上就是分配了一块内存,这里实际上使用的是C的malloc函数来动态分配一个堆空间,并且用void* 指针指向这个内存空间
第二步就是将这个指针执行类型转换,变成我们需要的之前指定的类型
第三步就是调用这个函数的构造函数,进行初始化
第一步是调用它的析构函数,清除它之前请求动态分配的内存空间。但是此时这个指针本身依然存在
第二步是释放它自己所占的内存,实际上是使用的C的free函数删除自己(指针)
首先,不论我们有没有array new搭配array delete,这个对象的本体都会被完整删除。
如果array new没有搭配array delete,那么析构函数将仅被调用一次,数组中其它的动态分配的内存将不会被回收,造成内存泄露。
1.类似于String这种带有指针的类,需要额外编写拷贝构造函数,拷贝复制函数,析构函数
2.拷贝赋值必须进行判断是否相同
3.使用数组new时需要搭配数组delete
//IDE:VS2019
#pragma warning( disable : 4996) //防止编译器强制要求使用strcpy_s
#include
using namespace std;
class String {
public:
String(const char* str = "") { //拷贝构造
if (str) {
ptr = new char[strlen(str) + 1];
strcpy(ptr, str);
}
else {
ptr = new char['\0'];
}
}
String(const String& str) { //拷贝构造
ptr = new char[strlen(str.ptr) + 1];
strcpy(ptr, str.ptr);
}
String& operator=(const String& str) {
if (&str == this) {
return *this;
}
delete[] ptr;
ptr = new char[strlen(str.ptr) + 1];
strcpy(ptr, str.ptr);
return *this;
}
char* getStr() const {
return ptr;
}
~String() { //析构函数
delete[] ptr;
}
private:
char* ptr;
};
ostream& operator << (ostream& os, const String& str)
{
os << str.getStr();
return os;
}
int main(void) {
String s1; //测试默认构造函数
String s2 = "shkfjkabc"; //测试拷贝构造
cout << s1 << endl;
cout << s2 << endl;
String s3 = s2; //测试拷贝赋值
String s4(s3); //测试拷贝构造
String s5("123asdas"); //测试拷贝构造
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
String s6; //测试连续赋值
String s7 = s6 = "12312asdvxcv";
cout << s6 << endl << s7;
}
在类中的数据或者函数前面加上static,就变成了一个静态变量/函数。
对于数据:每创建一个类的对象,它就会在内存中额外开辟一份空间
对于函数:存在一个隐藏的this指针,用来访问这个函数的调用者
对于数据:无论创建多少个类的对象,它始终只在内存中存在一份,所有对象共享
对于函数:将不再存在this指针,通常用来处理静态的数据
在STL中,cout继承自ostream,在ostream中对<<操作符进行了很多的重载
为了不让我们因为仅仅因为类中元素的数据类型不同而不得不重复声明类,C++提供了类模板,在声明时可以根据传入参数类型自动改变
和类模板的作用基本相同,都是为了我们不必再去编写更多地重复代码
当我们使用自定义的类(这里是Stone)时,由于模板在调用时是根据传入参数类型而改变的,所以会自动在Stone类中寻找有关操作符<的重载,这个过程也叫做引数推导。如果我们在Stone类中没有重载<操作符,那么会报错
namespace是一块有名字的逻辑区域,其内部有变量和函数等,形式如下。
namespce可以有以下三种用法
在这里表现为类queue中,有其他类(这里是deque)的对象,使得类queue可以使用deque已经完成的函数,避免重复造轮子。
这里的adapter是指一种设计模式,即适配器模式
从内存角度来看,在类queue中有一个deque对象,deque中有若干Itr对象。层层分解之后可以知道内存中所占用的大小
构造函数由内向外
析构函数由外向内
类似于装礼盒,构造函数先把里面物品放好,再进行包装。拆礼盒需要先拆外盒,再拿出内容物
类似于上面的复合,但是从类A中含有类B的对象变成了类A中含有指向类B对象的一个指针
1.由于是指针,他们的生命周期不再同步。一个在栈内存,使用完就自动销毁,一个在堆内存,需要程序员手动分配和释放
2.Delegation优点是可以实现如图所示pimpl(pointer to implementation)分离,它的对外接口(这里是类String)和内部实现类(这里是StringRep),也就是以后我们可以在保持不修改对外接口的同时对内部实现进行修改,不会影响客户,也称为编译防火墙
如图所示,_List_node继承了_List_node_base
子类包含了父类中的数据,比如_List_node_base中的两个指针,体现在子类不仅需要为自己的成员创建内存空间,也要为从父类继承来的成员创建空间
对于父类中的函数,实际上继承的是函数的调用权
名称 | 描述 |
---|---|
非虚函数 | 不希望子类对其进行覆写 |
虚函数 | 自己有一个已经存在的定义作为默认定义,但是允许子类对其进行覆写(override) |
纯虚函数 | 不存在默认定义,继承它的子类必须对其进行覆写(override) |
比如对一个形状类进行划分
1.它有一个固定的ID,这个应该不变,所以为非虚函数
2.它有一个默认的打印报错信息的函数,但是对于不同的形状而言,可以有多种不同的报错形式,所以为虚函数
3.它有一个绘画函数,对于一个叫做“形状”的形状,我们不知道怎么画它,当且仅当它的子类比如正方形,长方形对其进行实例化之后,我们才知道自己需要画一个什么形状的图形。所以它应该是一个纯虚函数
如果我们需要实现一个如下场景:
1.点击文件夹,在文件夹中找到你想到打开的文件,点击这个文件(OnFileOpen)
2.读取文件内容
对于动作1,无论你打开什么文件,操作都是一样的,都需要找到它的位置然后点开,区别在于怎么读取这个文件的内容,不同格式的文件,它的读取方式是不一样的。(Serialize)然后这两个动作应该是连贯的,所以我们把Serialize函数也放到了OnFileOpen函数里面去
在这里我们使用子类CMyDoc继承含有一个非虚方法(OnFileOpen)和一个虚方法(Serialize)的CDocument父类,其中子类需要对父类的Serialize函数进行覆写。
然后实例化一个子类的对象,它的执行过程就是下图。当它遇到OnFileOpen中的虚函数Serialize时,会自己去查找虚函数的实现方法。即提前写好已经固定了的函数,然后让子类去定义哪些暂时无法确定的函数。
测试代码如下:
#include
#include
int main()
{
//这一段是第一种情况的测试代码
//其中A是父类,B是复合类,然后C继承A并且复合B
using namespace std;
class A { //A是父类
public:
A() {
cout << "父类的构造函数被调用了" << endl;
}
~A() {
cout << "父类的析构函数被调用了" << endl;
}
private:
int a;
};
class B { //B是复合类
public:
B() {
cout << "复合类的构造函数被调用了" << endl;
}
~B() {
cout << "复合类的析构函数被调用了" << endl;
}
private:
int b;
};
class C:A { //C继承A,并且复合B
public:
C() {
cout << "子类的构造函数被调用了" << endl;
}
~C() {
cout << "子类的析构函数被调用了" << endl;
}
private:
int c;
B b;
};
C c;
}
#include
#include
//这一段是第二种情况的测试代码
//其中A是父类,与B复合,然后C继承A
int main()
{
using namespace std;
class B { //B是复合类
public:
B() {
cout << "复合类的构造函数被调用了" << endl;
}
~B() {
cout << "复合类的析构函数被调用了" << endl;
}
private:
int b;
};
class A { //A是父类,与B复合
public:
A() {
cout << "父类的构造函数被调用了" << endl;
}
~A() {
cout << "父类的析构函数被调用了" << endl;
}
private:
B b;
};
class C:A { //C是子类,继承A
public:
C() {
cout << "子类的构造函数被调用了" << endl;
}
~C() {
cout << "子类的析构函数被调用了" << endl;
}
private:
int c;
};
C c;
}
在编写UI时,可能会出现这种情况:用户对一个UI界面开了多个窗口,这些窗口可能显示同一种内容,但是它的表现形式不同,但同时程序员必须让这些内容中的数据保持一致。这就用到了观察者模式
在观察者模式当中,Observer就是去观察这个Subject的窗口,Subject可以拥有很多个Observer,表现在Subject中含有一个vector
同时Observer含有一个虚方法供子类进行派生,它的子类同样是Observer,可以放到vector
1.注册函数,用来添加观察者
2.通知函数,用于当更新了Subject时,对它所有的观察者中进行更新
3.删除函数,用于删除观察者(本例中省略了)
4.其它函数,用于处理Subject中的私有数据等等
在日常使用过程中,我们会使用到“目录”这种东西,它的特点就是目录中可以创建目录,目录也可以进行合并等等。总结一下就是用户可以使用多个简单的组件以形成较大的组件,而这些组件还可能进一步组合成更大的。它重要的特性是能够让用户一致地对待单个对象和组合对象。
在这张图中,可以大致分为一下三种
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
主要解决:在运行期建立和删除原型。
优点: 1、性能提高。 2、逃避构造函数的约束。
比如说子类LastSatIamge,我们从上往下对齐做一个解读(SpotImage同理)
数据: LSAT:LastSatIamge
它本身有一个静态对象LSAT(注:在下面添加一条实线的意思是这个成员为静态成员)
函数:
对于父类Image,同样做解读
数据:
prototypes[10]:Image*
用来保存原型,注意每种原形不论生成了几次,只会在这里面保存一个原型
函数:
1.clone():Image*
是一个纯虚方法,必须要子类对其进行实现,clone在子类中的作用是用来分配一个内存空间容纳子类的对象
2.findAndClone(i):Image*
在Image已经保存的原型里面找到我们需要创建的原型,调用这个原型的构造函数,返回生成的对象
3.addPrototype(p:Image*)
在子类中,要求返回他们的原型用来保存到Image的原型库当中去