侯捷——1.C++面向对象高级开发 总结
前面的几个视频没有总结,等以后有空再补
7. Class with pointer member(s) —— string 类
该string类,内含指针,所以要自己写构造函数和析构函数,不能使用默认的构造函数和析构函数。
包含指针的类,string类就是其中之一,不能使用默认的拷贝构造函数和拷贝赋值函数,要自己实现。 因为默认的拷贝构造函数和拷贝赋值函数只是简单地把用以拷贝的指针指向源指针指向的内容,就是两个指针指向同一个地方,这样不是真正的拷贝。所以要自己实现拷贝构造函数阿拷贝赋值函数。
7.1 string类的具体内容如下:
7.1.1 构造函数和析构函数
如果传进来的字符串指针指向空,则为m_data开辟一个字节,存放’\0’。
注意: 如果是class里包含指针,那么构造函数多半就要用到动态分配。所以析构函数要记得把动态分配的内存释放掉。
class中带有指针成员的,要自己实现拷贝构造函数和拷贝赋值函数!!!
当使用默认的拷贝构造函数和拷贝赋值函数时,会发生以上情况,两个指针指向同一块内容(这种属于浅拷贝),导致其中一块内存没有被指向,从而出现内存泄露。
7.1.2 拷贝构造函数(深拷贝)
要先动态申请内存,再把内容放到申请的内存里。
7.1.3 拷贝赋值函数(深拷贝)
这里的 this
表示的是等号左边。
当我们把函数的返回值换成void,且没有return *this时,可以满足单独赋值,就是a=b是可以的,但不能满足连续赋值,如a=b=c。
要如上面的代码所示,才能满足连续赋值。
一般情况下,传值尽量传引用,返回值也是,如这个拷贝赋值函数的返回值就是返回引用(String&)。我们再怎么判断什么时候该传值什么时候该传引用呢?
一般来说,只要不是本地变量就可以传引用。
在函数调用时,参数传递的方式有两种:按值传递和按引用传递。
按值传递是指将实参的值复制一份传递给形参,在函数内部对形参的修改不会影响实参的值。
按引用传递是指将实参的地址(即指针)传递给形参,在函数内部对形参的修改也会影响实参的值。
在C++中,本地变量是以栈的形式存在的,它们的生命周期只在函数调用期间有效,出了函数作用域后就会自动销毁。因此,如果我们将一个本地变量的地址传递给形参(即按引用传递),在函数调用结束后,这个本地变量就被销毁了,形参所引用的地址就变成了一个悬空指针,而且可能会导致未定义的行为甚至程序崩溃。
因此,通常情况下,我们不应该将本地变量的地址传递给形参,而应该传递指向堆内存的指针或引用。堆内存的生命周期可以通过手动开辟、释放控制,不会随函数调用的结束而销毁,因此可以安全地传递给形参。
注意:上面的代码中,一定要有检测自我赋值的代码!!!
因为当自己赋值给自己时,如果没有检测自我赋值的代码,第一步会将等号左边的指针指向的内存释放,这时,当执行第二步时,想要根据等号右边指向内存的大小去创建空间,当这时指向的空间已经不见了。
7.1.4 输出函数(output)
这个函数不能是成员函数,只能是全局函数。
8. 堆、栈与内存管理
Stack,是存在于某作用域的一块内存空间。例如当你调用函数,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。
在函数体内声明的任何变量,其所使用的内存块都取自上述的stack。
Heap,是指操作系统提供的一块全局内存空间,程序可从中动态分配获得若干块。
8.1 stack objects(栈对象)的生命期
c1便是所谓的stack object,其生命在作用域(scope)结束之后结束。这种作用域内的object,又称为auto object,因为它会被自动清理。
8.2 static local objects(静态局部对象)的生命期
c2便是所谓static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束。析构函数会在程序结束后才调用。
8.3 global objects(全局对象)的生命期
c3就是global object,其生命在整个程序结束之后才结束。也可以把它视为一种static object,其作用域是整个程序。
8.4 heap objects(堆对象)的生命期
p所指的就是heap object,其生命在它被delete之后结束。使用delete,背后会去调用对象的析构函数。
以上会出现内存泄漏,因为当作用域结束,p所指的heap object仍然存在,但指针p的生命会随着作用域结束而结束,所以作用域之外将再也无法访问到这个堆对象,但它又存在内存里,在其他程序中将会访问到或修改到该对象。导致这一现象是因为没有delete p。
8.5 new关键字
当使用new来创建对象时,会先分配内存,再调用构造函数。
new其内部调用malloc。
构造函数是成员函数,所以一定有this指针,谁调用构造函数谁就是this,上面是pc去调用,所以pc是this。
8.6 delete关键字
delete是先调用析构函数再释放内存。
delete内部会调用free。
8.7 动态分配所得的内存块
要强调一下,以下介绍的是在动态分配下的情况才是介绍的那样。
8.7.1 Complex class(复数类),in VC(在VC编译器下)
在VC编译器调试的情况(不同编译器,内存块大小有可能不同)下,一个复数类所占的大小是8字节(因为两个double成员变量),系统会给这个类前面多分配出8格(一个格代表4字节,所以总共32字节),在它后面分配4字节。除此之外,还会在它的头尾增加红色的两块(称为cookie)。在VC下,动态分配的内存一定要是16的倍数,所以上面本来系统会分配52个字节,而因为要16的倍数,就只能填补12字节(3个绿色格子),凑成64字节。
为什么要多出这么多空间呢,不是造成空间浪费吗?
这些空间浪费是必要的,因为操作系统在回收内存的时候,需要靠这些来顺利地回收。
当在非调试情况下,不会有灰色部分,只有复数类的大小和cookie的大小,加起来就是16字节,刚好是16的倍数,不用再进行填充。
cookie(红色部分)的作用
为了记录分配空间的大小和整块空间的状态。如上图在调试状态下的复数类,分配了64字节,转换成16进制是0x40,但cookie记录的是0x41,是因为状态是借用最后一个bit(位)来表示的,'1’代表是已经分配出去了,'0’代表还没分配出去。
8.7.2 String class(字符串类),in VC(在VC编译器下)
在调试状态下,String类的大小是4字节(因为一个成员变量,而是一个指针,为4字节),调试状态下固定多出32+4字节,还有两个cookie,加起来总共48字节。
在非调试模式下,是12字节,为了满足是16的倍数,填充了4字节。
8.8 动态分配所得的array(数组)
8.8.1 Complex class数组
在调试模式下,一个Complex占8字节(两个double,一个是实部,一个是虚部),Complex[3]会分配24字节(3*8),调试模式会在前后多出36字节(32+4),还有前后两个cookie(8字节),在VC编译器下,数组前还会又4字节存放一个整数,来表示数组的长度。所以全部加起来72字节,为了满足16的倍数,填充8字节,凑成80字节。
非调试模式就不多赘述了。
8.8.2 String class数组
string类同理,不再赘述。
8.8.3 总结
array new 一定要搭配 array delete,即 new [] 要搭配 delete[]。
当写 delete[] p
时,编译器才知道要去释放的是一个数组,才会根据数组大小去调用对应次数的析构函数。
当只写 delete p
,没写[]时,编译器不知道你要释放的是一个数组,只会释放一次,如图,就只会释放第一个,而第二第三都还没被释放。当delete隐含的两个步骤,再释放完数组后,会在把p指向这整块内存释放。这时会导致内存泄露,因为第二第三都还没被释放。
当上面的例子是Complex类时,不用 delete[] 也没事,因为它每个对象都不是指针,不是动态分配的,使用delete p,它就会根据数组大小去释放每一个对象。 而String类则不同,它的每个对象里都有指针,且是动态分配的。
10. 扩展补充:类模板、函数模板及其他
10.1 this指针和补充static
当类中有静态成员时,静态成员跟对象是脱离的。静态成员没有this指针。
补充: 静态成员变量和静态成员函数是属于整个类的,而不是属于某个具体的对象。它们只有一份存储空间,所有的对象共享它们。因此,静态成员的初始化需要在类外进行。
一般来说,静态成员的初始化有两种方式:
在类外初始化静态成员变量:
// 在类外定义并初始化static变量 int MyClass::static_var = 0;
类内声明静态成员变量,但是需要在类外进行初始化:
class MyClass { public: static int static_var; // 声明静态成员变量 }; int MyClass::static_var = 0; // 初始化静态成员变量
需要注意的是,如果静态成员变量是 const 类型或者枚举类型,则可以在类内进行初始化。例如:
class MyClass { public: static const int const_var = 123; // 在类内使用静态成员变量初始化>const成员变量 };
而静态成员函数则不需要在类外进行初始化。
调用static函数的方法有两种:
- 通过对象调用(但这种调用时,编译器不会帮我们做隐藏操作:把主动调用的对象的地址,就是this指针,传进去);
- 通过类名调用;
10.2 类模板(class template)
10.3 函数模板(function template)
10.3 命名空间(namespace)
11. 组合、委托与继承
11.1 Composition组合(has-a)
一个类里有其他的类作为成员,这种情况叫做组合。
adapter(适配器)的解释
如上图,queue(队列)的函数实现都是依靠deque(双向队列)实现的,都是调用deque的接口。这种情况,queue就是adapter。
看到这种图,要知道它表达的就是Composition(组合)。黑色菱形这一端表示容器,它容纳拥有它箭头指向的那个东西。
11.1.1 组合的大小计算
需要根据类中包含的其他类去计算大小,之后再相加。
11.1.2 组合关系下的构造和析构
注意: 上面红色的地方都不用我们自己写,除非特殊情况,是编译器默认会帮我们加上的。
构造函数由内而外: 先调用“组件”的构造函数(注意: 调用“组件”的构造函数是默认的构造函数,当调用者不想编译器调用默认的构造函数时,红色部分就要自己写,并为“组件”的构造函数传递参数),再调用“容器”本身的构造函数。
析构函数由外而内: 先调用“容器”本身的析构函数,再调用“组件”的析构函数。
11.2 Delegation(委托)
pimpl
:pointer to implement(有一根指针指向为我实现一些功能的类)。也叫Handle/Body。左边是Handle,右边是Body。
绿色的点表示字符串对象中的StringRep指针。三个字符串都指向相同的字符串,所以StringRep中的count为3,充当引用计数器。
11.2.1 写委托的好处:
Handle是对外开放,给用户进行使用;Body可以随意切换,也就是说,Handle里的指针可以去指向不同的实现类。这就会有一种弹性,就是说Body这边无论怎么变动,都不会影响Handle。客户怎么看这个字符串都不受影响。这种手法又叫做编译防火墙。
11.2.2 委托的图表示
注意这里的图,菱形是空心的,这种关系就是委托。String类中包含StringRep类的指针,可以这样理解,String拥有StringRep,但比较虚,没有组合那样实,因为指针,你不知道什么时候才有(有确实地指向)。
11.2.3 copy on write(写时复制)
这种情况是,多个字符串共享同一个字符串空间,当其他一个想要修改时,想要重新开辟空间并把字符串复制过去,再做修改。
11.3 Inheritance(继承)(is-a)
箭头是由子类指向父类,且箭头是空心的,T是表示这个类是模板类。
11.3.1 继承关系下的构造和析构
子类里包含着父类的成分。
构造由内而外: 子类的构造函数首先调用父类的默认构造函数,再执行自己的构造函数。
析构由外而内: 子类的析构函数先执行自己的,再调用父类的析构函数。
这里的谁先谁后编译器会帮我们去处理。
注意: 析构由外而内的前提是,父类的析构函数要设置为virtual(虚析构函数),否则会出现未定义行为(undefined behavior)。
12. 虚函数与多态
继承要搭配虚函数使用。
non-virtual(非虚)函数:你不希望子类重新定义(override,覆写)它。
virtaul(虚)函数:你希望子类重新定义它,且你已经对它有默认定义。
pure virtual(纯虚)函数:你希望子类一定要重新定义它,你对它没有默认定义。
其他例子:
把父类中的Serialize()的实现延迟到子类中才实现。
当主函数执行myDoc.OnFileOpen()时, 从编译器的角度,相当于执行CDocument::OnFileOpen(&myDoc),会把调用该函数的对象的地址作为隐藏参数传进来,这个就是this point(this指针),所以在执行OnFileOpen()时,执行到里面的Serialize()时,执行的是this->Serialize(),而this是CMyDoc类的对象,所以会调用该类实现的对应的函数。其中的原理是虚函数指针。
12.1 继承+组合关系下的构造和析构
-
第一种情况:
子类继承父类,子类中有组合。
构造的先后顺序: 继承关系的还是按照由内而外,先调用父类(Base)的构造函数,再调用子类(Derived)的构造函数。当子类中又有组合关系,组合关系是先构造其他成员对象(如Component),再构造自己。所以总体的顺序是,先父类(Base),再组合(Component),最后是子类(Derived)。析构的顺序则刚好相反。
代码如下:
#include<iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base" << endl;
}
~Base() {
cout << "~Base" << endl;
}
};
class Component {
public:
Component() {
cout << "Component" << endl;
}
~Component() {
cout << "~Component" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived" << endl;
}
~Derived() {
cout << "~Derived" << endl;
}
private:
Component c;
};
int main() {
Derived d;
return 0;
}
输出结果是:
Base
Component
Derived
~Derived
~Component
~Base
-
第二种情况:
父类中有组合,子类继承父类。
构造的先后顺序: 父类(Base)中有组合,所以是先调用组合(Component)的构造函数,再调用父类(Base)的构造函数。继承关系,则是由内而外,最后调用子类(Derived)的构造函数。所以总体顺序是,先调用组合(Component)的构造函数,再调用父类(Base)的构造函数,最后调用子类(Derived)的构造函数。析构函数的顺序则与构造的顺序相反。
代码如下:
#include<iostream>
using namespace std;
class Component {
public:
Component() {
cout << "Component" << endl;
}
~Component() {
cout << "~Component" << endl;
}
};
class Base {
public:
Base() {
cout << "Base" << endl;
}
~Base() {
cout << "~Base" << endl;
}
private:
Component c;
};
class Derived : public Base {
public:
Derived() {
cout << "Derived" << endl;
}
~Derived() {
cout << "~Derived" << endl;
}
};
int main() {
Derived d;
return 0;
}
输出结果:
Component
Base
Derived
~Derived
~Base
~Component
12.2 委托+继承(功能最强大)
这两种结合起来的适用场景:
当一个东西用多个窗口去看,看到的是同一个东西,且对其中一个做出修改,其他窗口中会跟着改变。
Observer(观察者)相对于窗口,用于观察Subject(物体)。Subject与Observer的关系是一对多的关系,因为窗口可以有多个。可以开出很多个窗口来观察Subject。
13. 委托相关设计
13.1 假设要实现一个文件系统
就是一个目录中即有文件,也有目录,然后目录里又有文件,也有可能有目录。这种情况我们可以采取下面这种方案来解决:
假设 File(文件)类我们用 Primitive
类来实现,目录类用 Composite
来实现,因为 Composite
类是实现了目录,而目录即要能装目录,也要能装文件,所以 Composite
要有一个容器,要既能放 Composite
,也要能放 Primitive
。想要实现这种容器,可以实现 Component
类作为父类,然后让 Primitive
和Composite
都去继承它,也就是让文件类和目录类都有相同的父类,然后让这个容器存放父类这种类型的指针,这样子容器就可以即放Primitive
类,也可以放Composite
类。
注意: C++容器只能存放一样大小的东西,所以上面也采用了指针,指针大小相同。不过也是为了能存放两种大小,所以采用存放他们父类的指针。
13.2 假设想要一个树状树形体系
假设想要一个树状树形体系,想要创建未来才出现的类(就是要使用别人已经搭好的框架,就是下图中的父类,表示框架,而又要往下派生,且要父类知道这些子类),这个时候应该怎么办?
曲线上的类是当前已经实现的,曲线下面的是未来才会派生下去的。而目前这个父类想要创建下面派生出的类,而下面的类有可能几年后才会有人去写,有一个办法就是:
让下面的子类自己创建出自己的对象来,作为原型(Prototype)。让父类可以看到下面子类的原型放在什么位置上,然后父类就可以去复制它,就等于父类自己在创建了。
我们先分析其中一个子类。
该类中,创建了一个静态的对象LAST(在UML图中,变量的写法是,变量名在前,变量类型在后,如 LAST:LandSatImage
,变量名下面加了下划线,表示该变量是静态变量),还有私有的构造函数(在UML图中。函数前面加 '-‘号表示private,‘+’号表示public,要是没加就是默认’+'号,加‘#’号表示protected),保护的带参构造函数和公有的clone()(拷贝函数)。
注意: 静态对象会调用构造函数,而构造函数里有 addPrototype(this)
,这个是父类的函数,会把这个静态对象作为原型,就是this,作为参数,把它保存到父类中的prototype
数组中。
以上图片中父类的源代码:
其中一个子类的源代码:
14. conversion function(转换函数)
黄色部分就是转换函数,要转换的类型就写在函数名,不用写参数,不用写函数类型。因为这个函数不能去改变分子分母的值,所以还要加上const
。
转换函数被调用的过程:
当执行 double d=4+f;
时,首先执行到 +
号时,编译器会看看Fraction类有没有实现operator+()这个函数,且参数类型符合+
号两边的参数是否符合实现的这个函数的类型。显然这个类没有,所以编译器就得重新考虑一下其它方法,发现只要把 f 转换成double类型就好,刚好类中有实现这样的一个函数,所以它就会去调用转换函数。
15. non-explicit-one-argument ctor(非显示单实参构造函数)
当执行 d2=f+4;
时,编译器会去参考类有没有实现operator+
,发现有,operator+
是作用于左边的,也就是 f 。并会把4
转换为Fraction
,因为Fraction里的den
是默认参数,也就是可以只传一个参数。
所以non-explicit-one-argument ctor
是可以把别的东西转换成本身这种东西的构造函数。
15.1 conversion function vs non-explicit-one-argument ctor
如果上面介绍的两个函数都并存的话,编译器就不知道怎么办了。
15.2 explicit-one-argument ctor(显示单实参构造哈数)
当构造函数前加上 explict
后,编译器就不会把 4 自动转换成 一分之四了,也就是不会在执行 d2=f+4
时,自动调用把 4 作为参数传入构造函数中。 explict
意思就是要明确指定显示地调用构造函数才会调用。所以在执行这个加法语句时,就会失败,因为加法要求左右两边都是Fraction类型,而 4 转不过去,所以失败。
注意: 大部分情况,explict
这个关键字主要用在构造函数前面。
16. pointer-like classes(模仿指针的类)
16.1 关于智能指针
操作符重载是C++中很重要的一个知识点。而智能指针下面两个操作符的重载基本都是这样写。
当执行到上图右边中的 shared_ptr<Foo> sp(new Foo);
时,就是px=sp,相对于图片中px指向一个新创建的Foo匿名对象且px=sp。
当执行 Foo f(*sp);
时,想要对sp解指针,所以需要调用到 operator*()
,该函数的返回内容应该是sp指向的东西,也就是px指向的东西,所以是 return *px;
。
当执行 sp->method()
时,会调用 operator->()
,而这个函数会返回px,根据C++的语法规则,还会再执行px->method()。
16.2 关于迭代器
迭代器可以理解是一种智能指针,它需要重载很多符合。
17. function-like classes(像函数的类,也就是仿函数)
函数的操作符就是()
,所以写仿函数就是要重载()
。
标准库中的一些仿函数:
上面这些仿函数继承的类并不相同。
标准库中仿函数所使用的 base class(基类),unary_function
用于一个操作数的,binary_function
用于两个操作数的。
以上两个类的大小理论上是0。
18. namespace 经验谈
适用场景:当团队开发时,可能出现没有交流,每个人独自开发一部分程序的情况,这时就需要用到 namespace(命名空间)。每个人把自己写的程序通过namespace包起来从而起到区分的作用。
19. class template(类模板)
类模板在使用时,需要使用尖括号指明使用的类型。
20. function template(函数模板)
注意: 上面两种模板的尖括号中typename和class的区别,可以看这篇文章。
函数模板在调用的时候不用像类模板一样需要用尖括号指明,直接调用就行。编译器会对其就行实参推导。
21. member template(成员模板)
黄色部分就是成员模板。因为黄色部分在 pair 结构体中就是一个 member,而自己本身就是一个template,所以就称为member template。
成员模板一般出现在标准库。
Base1(鱼类) 和 Derived1(鲫鱼)是一种继承关系;Base2(鸟类) 和 Derived2(麻雀)也是一种继承关系。
把一个由鲫鱼和麻雀构成的pair
,放进(拷贝到)一个由鱼类和鸟类构成的pair
中是可以的。反之则不可以。因为成员模板中,鲫鱼相对于p.first
,它赋值给类的first
,就是鲫鱼赋值给鱼类,向上转型是可以的。但反过来就不行。
这种成员模板在标准库中是常见的,因为这样它可以使代码更有弹性。
22. specialization(模板特化)
泛化的反义词就是特化。泛化就是下面图片中的上部分,没有直接指定类型,是一个模板。
特化的写法如下图,template后的加括号里为空,直接在hash
后指定类型。当使用的时候就会根据指定的类型调用对应的代码。
上面绿色部分hash<long>()
是创建一个临时对象,它后面的括号才是调用它重载的函数。
23. partial specialization(模板偏特化)
23.1 个数上的偏
可以先绑定vector的模板类型。Alloc的等号后面是默认值。
模板偏特化,个数上的偏是指在模板参数中,先绑定其中前几个,要按顺序绑定。如有五个模板参数,只能按顺序绑定1、2、3,这前三个参数。不能说绑定1,3,5,不可以是跳着绑定的。
23.2 范围上的偏
24. template template parameter(模板模板参数)
XCls类中私有的对象 c 是拿第一个模板参数T作为参数。
c的T指的是XCls类的第一个模板参数T,而不是第二个模板参数Container中的模板参数T。在这个类的定义中,Container是一个模板类模板参数,它本身需要一个类型参数,因此需要定义一个对应的模板参数T,这个T用来给Container模板类传递类型。而在类的私有成员中,通过使用Container< T> 来实例化Container类模板,将T传递给Container,从而定义了成员c的类型。所以,c的T指的是XCls类定义时声明的第一个模板参数T。
没办法通过是因为容器一般都有第二模板参数,甚至第三模板参数。虽然它们都有默认值,但是就是语法过不了。
想要通过,需要引入另外一个语法。
这样就能通过,但上面的是C++2.0的语法。
上面不可以通过的是因为容器需要多个模板参数。而智能指针有的只需要一个模板参数,所以就可以通过。
注意: 下面这个就不是模板模板参数。
因为传 list 时已经是让它绑定好 int 了,已经不是模板了,所以它并不属于模板模板参数这种类型。
25. 关于标准库
25.1 测试编译器支持C++的方法
26. variadic templates(since C++11)数量不定的模板参数
自从C++11之后,就支持这种数量不定的模板参数,可以理解为是一个参数和一包参数。…
表示一包,就是它可以没有,可以一个,也可以是多个。
print(7.5,"hello",bitset<16>(377),42);
首先会调用上面第二个print
函数,输出7.5,在把后面一包递归调用第二个print
函数。直到最后输出42时,一包的大小为0,则编译器会去调用第一个print
函数。
计算这个一包的大小可以采用sizeof…(args)
来计算。
27. auto(since C++11)
在没有一开始就初始化的情况下,是不可以使用auto
的。
28. ranged-base for(since C++11)
29. reference(引用)
从内存的角度来解析引用。
所有的编译器,对待引用,也就是int& r=x;
,都是用指针来实现的。这个语句相当于给 x 起了一个别名,也就是 r 代表 x,底部实现就是 r 有一根指针指向 x。
使用引用的注意事项: 在声明的时候,就要初始化。而且初始化完后就不可以改变了。例如上面r = x2;
,这个语句是错误的。
注意: 对象和其引用的大小相同,地址也相同(但这些都是假象,因为底层实现是指针,但编译器会制造这种假象,是好的)。至于上面的例子就是 sizeof( r )==sizeof( x ) 和 &r ==&x。
29.1 reference的常见用途
reference通常不用于声明变量,而用于参数类型和返回类型的描述。
func2(obj)
和func3(obj)
虽然形式差不多,但其实func3(obj)
在调用的时候比较快,因为引用底部是指针,无论对象多大,它只要传指针就行,不用重新去创建对象,而传value的话,需要创建新对象,把值复制给新对象。
same signature(函数签名)是不包括函数类型的。但函数签名不会去区分有没有&
这个符号的。所以上面那种情况是不能同时存在的。
还有一个问题就是const(指的是函数的花括号前面的const,就是上面灰色部分那里加上)是函数签名的一部分。
在C++中,将函数的参数列表后的const关键字称为常量修饰符,它可以用来指定函数不会修改类的数据成员。如果使用const来修饰函数,则在函数体内无法改变对象中的任何成员变量,这种函数被称为“常成员函数”。
当我们在类中声明一个常成员函数并将其定义时,在函数名后面加上const修饰符即可:
class Example {
public:
int getValue() const {
return myValue;
}
private:
int myValue;
};
上面的例子中,getValue函数被标识为常量。这意味着在函数体内,我们不能修改对象的任何成员变量。
30. Object Model(对象模型)
30.1 inheritance(继承)关系下的构造和析构
红色部分是编译器自动帮忙加上去的。红色部分的构造和析构函数都是默认的构造和析构函数。
30.2 composition(复合)关系下的构造和析构
Container里拥有Component。
30.3 inheritance+component关系下的构造和析构
30.4 关于vptr(虚指针)和vtbl(虚表)
只要类里有一个虚函数(就算是多个),该类的对象的大小中就会多一根指针(需要占用空间),该指针指向虚表。这个指针只跟虚函数有关系,跟一般函数没有关系。
虚表里存放的都是函数指针,指向虚函数。
上图的n
表示的是第几个虚函数,它的顺序会根据类中写的顺序来。例如类A中,vfunc1写在第一中,则它是第零个。
因为容器只能存放一样大小的东西,所以采用存放的是指针,指针大小就相同。
动态绑定的三个条件:
- 必须通过指针调用
- 指针可向上转型
- 有虚函数
30.5 关于this
在C++中,所有的成员函数一定有一个隐藏的this指针作为隐藏参数。例如,上图中的OnFileOpen()
中的Serialice()
的调用动作是this->Serialise()
。
在调用虚函数时,会看this指向哪个对象,从而去调用对应对象的虚函数。
30.6 关于动态绑定(Dynamic Binding)
上图中红框部分我们会考虑 a 是调用 a 的 vfunc1() 还是 b 的 vfunc1()。其实是 a 的,因为它不满足动态绑定的条件,不是指针,所以它属于静态绑定,它本身是什么就是调用对应的。
上面紫色的部分就满足动态绑定的三个条件,所以它们属于动态绑定。
31. 谈谈const
上面红色位置加的const
只能加在成员函数的后头,不能加在全局函数中。
在成员函数后面加const
的意图是,告诉编译器,这个成员函数不打算改变类中成员变量的数据。
const object(常量对象)表示data members(数据成员)不得改变。
non-const object(非常量对象)表示data members(数据成员)可改变。
const member functions(常量成员函数)保证不改变data members(数据成员)
non-const member functions(非常量成员函数)不保证data members(数据成员)不变。
常量对象可以调用常量成员函数。
常量对象不可以调用非常量成员函数。
非常量对象可以调用常量成员函数。
非常量对象可以调用非常量成员函数。
一般print()只是输出动作,不会去改变数据成员的值,所以一般要把它设置为const。如果没有加,而定义的字符串是常字符串,就会调用失败。
这是函数重载,重载不看函数返回类型,只看函数签名,而const
属于函数签名的一部分,所以上面两个函数不同。
上面两个函数都是C++中string类的成员函数,当编程中,多个字符串赋值都相同的话,会共享同一个字符串。而当需要改变字符串中的某个字符时,就要考虑copy on write(写时复制)。当本来就是常量字符串时,就不用考虑。
32. 重载 ::operator new、::operator delete、::operator new[]、::operator delete[]
加了::
这个符号,是表示全局的意思。当使用new
和delete
关键字时,如果自己有重载就会跑到重载的这里来;如果没有,则调用默认的。
关键字new
和delete
,底部会去调用operator new和operator delete。
33. 重载member operator new/delete(类中的成员operator new/delete)
33. 重载member operator new[]/delete[]
34. 示例
如果使用的类,其内部有重载operator new/delete,直接使用new和delete的话,就是使用重载的。如果需要使用全局的,则需要在new
和delete
前面加上::
。
类Foo的大小,int占4字节,long也占4字节,string底部是指针所以也是4字节。所以加起来就是12字节。
当使用的是new[]时,会多出4个字节来存放数组元素的个数(计数器)。
在有虚函数的情况下,由于有虚指针,指针占4个字节,所以需要加4。
new[],构造函数是从上往下调用的,this指针会随着移动。
delete[],析构函数是从下往上调用的。
35. 重载new()和delete()
只有当new用的构造函数抛出异常,才会调用这些重载版的operator delete()。new会先分配内存再调用构造函数,而如果构造函数抛出异常,就会调用对应operator delete(),把分配的内存释放掉。