C++面向对象高级编程-上

C++面向对象高级编程

header头文件防卫式声明

image-20211209230451166

这是一种防卫式声明,防卫式声明的作用是:防止由于同一个头文件被包含多次,而导致了重复定义。防卫式声明表示,如果__COMPLEX__没有被定义过,那么就展开定义,否则跳过。

__COMPLEX__被称为预处理器变量一般有两种状态:已定义或未定义。

  • #ifndef 指示检测指定的预处理器变量是否未定义,如果未定义,那么跟在后面的所有指示被处理,直到出现#endif;如果已定义,那么#ifndef测试为假,该指示和#endif指示间的代码都被忽略。
  • #define指示接受一个名字并定义该名字为预处理器变量。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- "Car.h",代码如下(并没有添加防卫式声明):
// Car.h
class Car
{
// ...
};
-- "Person.h",代码如下(包含了Car.h文件):
// Person.h
#include "Car.h"
class Person
{
public:
Car car;
};
-- 在"main.cpp"中,我们同时include两个头文件:

// main.cpp
#include "Car.h"
#include "Person.h"
int main(int argc, const char * argv[]) <br>{
Person p;
}
此时,我们会发现编译出错:Redefinition of 'Car'.

我们需要知道,在预编译阶段,编译器会把.h文件展开,即main.cpp中的代码可以看做是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car
{
// ...
};

class Car
{
// ...
};

class Person
{
public:
Car car;
};

int main(int argc, const char * argv[]) {
Person p;
}

模板简介

我们如果希望double类型的实部和虚部变成int,那么我们需要重写一个几乎一样的complex类,这是我们所不希望的:

image-20211209231326729

因此出现了模板:

image-20211209231508596

我们先把类型设为T,使用时根据所写的类型再将T绑定为各种类型。

inline内敛函数

首先了解一下什么是内敛函数?

内联函数(有时称作在线函数编译时期展开函数),用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。

  • 如果你的函数写在class定义里,那么这个函数会成为inline函数的候选人,具体内敛与否还要看编译器自己。

image-20211209232037127

  • 你也可以给函数前加上inline,但是这只是你对编译器的建议,具体内敛与否还要看编译器自己。

access level访问级别

private,public,protected的访问范围:

private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数

类的继承后方法属性变化:
使用private继承,父类的所有方法在子类中变为private;
使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
使用public继承,父类中的方法属性不发生改变;

构造函数的初始化列表

C++ 类构造函数可以用初始化列表

1
complex(double r=0,double i=0):re(r),im(i){}

等价于

1
complex(double r=0,double i=0){re=r;im=i;}

但我们尽量用第一种,第一种写法更正规,这是构造函数特有的写法(指:re(r),im(i){})。

构造函数的重载overloading

函数是可以重载overloading的,即参数不同,但是函数名相同,调用函数时编译器会自动选择参数对应的函数,重载的几个同名函数本质上都是各不相同的函数

image-20211209234150840

这种是正确的吗?

image-20211209234407554

比如定义一个complex c此时编译器发现这两者都可以使用一个是全是默认参数,另一个是无参,编译器认为都可以,所以此时这两者是不能一起存在的。

构造函数放在private的意义—单例模式

image-20211209234847158

此时是不能调用构造函数的,所以这种做法一定不可能出现吗?并不是,当你不希望被外界创建对象时可以使用,那么这样的一个类还有什么用呢?单例模式。(下面内容建议先浏览static静态:静态变量,静态函数这一节)

没有任何人可以创建A类,A类只有一个对象a一开始就被创造好等待被初始化。

  • 外界想获得这唯一的a:通过getInstance()函数

  • 外界想修改这个唯一的a:通过A::getInstance().setup(...);

image-20211209235038633

常量成员函数const

成员函数可以分为两种:会改变数据的和不会改变数据的,不会改变数据的十分建议加上const,表示这个成员函数不能改变成员变量。

image-20211209235254760

不管你的函数加没加const,这么用是没问题的:

image-20211209235536414

但是如果一个const类型的对象 real()imag()函数没加const,下面这种用法就是错误的:

image-20211209235629111

因为希望complex的成员变量是不可以被改变的,而你的real,imag函数却不是const类型的,表示你有可能会改变他们,因此编译器不会让这种危险的事情发生,编译错误。

最后总结:

除了下面的表格总结外,我们还有一条c++中的重要法则:

当成员函数const和non-const版本同时存在,const对象只会调用const版本,non-const对象只会调用non-const版本。

image-20211220021543751

传参:by value/by reference

image-20211210000143936

引用占四个字节的内存(本质是一个指针)。

我们尽量都传引用,因为他普遍来说更快,但是引用有可能会改变原来的值,如果我们不希望这种情况发生,那么可以利用(const complex&)的方法,使得参数无法修改。

pass by reference(to const)的一个小例子

1
2
3
4
5
6
7
/*pass by reference*/
int c = 10;
int &a = c;
a = 5;
cout<<c<<endl;//输出5,这个引用时可以修改引用值的

/*pass by reference(to const)*/

友元friend

一份更仔细的介绍: https://www.cnblogs.com/zhuguanhao/p/6286145.html

image-20211210001054480

友元不是成员函数,但是它可以访问类中的私有成员,但他会破坏c++类的封装性,尽量少用,规范的用。

相同class的各个对象互为友元

看一下下面的问题:没有friend的字眼,为什么func可以直接访问private里的实部和虚部呢?

image-20211210001343468

可以这么解释:相同class的各个对象互为友元

this和重载运算符函数的设计

这两者等价,this是一个隐藏的指针(但你不能在参数列写出来,this是已经默认存在的),谁调用这个函数谁就是this

image-20211210003518533

对于重载运算符,如果我们只用c2+=c1这种,那么你写:

1
2
3
4
inline complex& complex::operator+=(const complex& r)
{
return __doapl(this,r);
}

或者

1
2
3
4
inline void complex::operator+=(const complex& r)
{
__doapl(this,r);
}

都是ok的,但是我们要思考到:

很多人会有这种用法: c3+=c2+=c1 这种运算方式是指c2 +=c1,c3+=c2 ,此时对于第二种返回void的写法就出了问题,因为c2+=c1执行完成后这个东西本身要当成右值,然而却返回了void,因此会出错。

所以我们总是建议写第一种。

重载运算符函数的常见用法/规则

image-20211210201248027

这里为什么选用return by value呢?

因为 a = b+c+d 在c++中b,c,d做完值是不可以变得,return by reference会导致b,c变化。

如何区分加减和正负?

根据参数的数量即可。

image-20211210202140742

== !=的设计标准

image-20211210202419885

typename()临时对象

typename(...)就是建立一个临时对象,这个对象不需要名字,用完即毁。标准库经常会有这种用法。

image-20211210201853227

image-20211210202006287

运行完当前行后 complex()complex(4,5)即消失

<<的重载

image-20211210202911883

上述的ostream& os为什么不加上const呢?

这是因为在

1
return os<<"("<<real(x)<<","<<imag(x)<<")";

os的状态是不断在变化的。

重载<<的函数返回值可以加const吗?

不可以,因为这里在不断改变cout的状态:

image-20211210203457500

拷贝构造函数/析构函数

拷贝构造函数如果你不写编译器会默认帮你写一个,即单纯的一个个把成员变量复制过去,但是对于字符串,里面的变量是指针,就会拷贝完后两者都指向同一个字符串。默认的构造函数是有风险的:如果你修改原对象中的字符串,会导致拷贝得到的那个对象字符串也发生变化。

image-20211210204953451

析构函数:

class里有指针我们大多数会做动态分配内存,动态分配的话在变量生命结束之前要释放空间。

image-20211210205056346

delete对象的指针就是:将指针所指向的内存空间回收

image-20211210205355524

含有指针的类必须要拷贝构造函数和重载赋值=函数

如果你不写编译器会默认帮你写一个,即单纯的一个个把成员变量复制过去,但是对于字符串等里面的变量含有指针的类,拷贝/赋值完后两者都指向同一个字符串。

例如下面 world就造成了内存泄漏

image-20211210205903468

因此对于重载=函数,需要

1.先清空自己,防止内存泄露(这点一定要注意,因为可能存在s1 = s2, s1 = s3, s1 = s4这种多次赋值操作,如果不清空每次提前自己就会导致自己之前new出的内存找不到了,导致内存泄漏)

2.重新分配空间

3.给新空间赋值

image-20211210210133496

同时要注意到自我赋值问题,即a = a这种傻瓜的用法,但可能确实存在这种用法,因此此时return 自己即可。如果你不写自我赋值检测,那么做以上那么三步,会导致结果出错。

堆,栈与内存管理

image-20211210211452869

c1存在里,程序离开变量作用域后会自动释放内存。

而p指针所指向的内存时new开辟的空间,是存放在里的,需要手动释放空间。

什么是内存泄露呢?

指针生命结束了,但指针所指的堆中的空间仍然存在。

image-20211210225514119

new一个对象的原理

image-20211210225938299

所以new一个类的对象内部其实做了三件事情:

1.operator new() 分配空间并返回一个void*类型指针p

2.void*类型 强转(通过static_cast) 成为目标class类型指针p

3.指针p调用构造函数。

TIPS of operator new()

operator new():指对new的重载形式,它是一个函数,并不是运算符。

对于operator new来说,分为全局重载和类重载,

1.全局重载是void* ::operator new(size_t size)

2.在类中重载形式 void* A::operator new(size_t size)。

还要注意的是这里的operator new()完成的操作一般只是分配内存,事实上系统默认的全局::operator new(size_t size)也只是调用malloc分配内存,并且返回一个void*指针。而构造函数的调用(如果需要)是在new运算符中完成的

delete一个对象的原理

先调用析构函数(~fun()),再释放内存(调用free())

image-20211210230710412

new [ ]要搭配delete [ ]

image-20211210231520440

如果new array不搭配delete array很可能会造成内存泄漏:

下图例子,我们调用delete 其实都会删除掉那一块部分,因为开头的cookie(即下图的21h)记录了这一块的大小,这一块是没有问题的。

问题就出在对于array中的每一个对象,delete[]会对每一个对象都调用析构函数,而delete只会对array的第一个对象调用析构函数,如果对象中是一个指针指向外部空间,那么析构函数就没有回收那部分的内存空间,导致内存泄漏

image-20211210232317492

static静态:静态变量,静态函数

首先要了解对于一个非静态(non-static)的成员函数,他要怎么面对各种各样的对象调用他呢?通过this指针获得对象的数据,然后在自己的函数逻辑中进行计算。如下图:c1,c2,c3分别各自拥有this指针,可以通过this指针调用函数。

image-20211210233750163

那么下面先讨论static变量:

比如银行的利率每年调整,每个人的账户都加入一个利率显然是没必要的重复数据,修改也不方便,那利率就可以设置为static 数据,自己独自一份。

static静态函数:

他只能处理静态数据

static 称为类变量,它属于这个类,而不属于这个类的实例对象(但是可以通过实例对象去访问),当类链接的时候就为其分配空间并初始化默认值,空间分配在堆中。

image-20211210234931612

调用static函数方式有两种:可以通过对象调用或者类名直接调用。

image-20211210234550827

单例模式可以有这种写法

image-20211209235038633

尝试更好的写法:static变量写在函数里,即即使你一开始创建了A a,但没有人用这个单例,依然不会占用空间。

image-20211210235828020

自己写的一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A
{
public:
int x,y;
static A& getInstance()
{
static A a;
return a;
}
private:

A(int xx = 0, int yy = 0): x(xx), y(yy) {}
};

int main()
{

cout<<A::getInstance().x<<endl;

return 0;
}

函数模板中的参数推导(argument deduction)

image-20211211001053901

namespace命名空间

对标准库里的东西全部打开,以后不用写std::cin等 直接写cin即可

image-20211211001343326

也可以部分展开namespace:

image-20211211001503678

复合(composition)

复合就是has-a的关系,比如class A中有Class B的对象。其实C语言结构体是我们见过最早的复合了。

下面就是通过复合实现了一个adapter模式:

image-20211211002417294

复合的内存问题:

image-20211211002806030

复合的构造函数和析构函数的调用顺序:

构造函数调用顺序是先内部再外部,而析构函数相反。

除此之外,调用外部构造函数的时候先调用的是内部default构造函数,如果你内部有许多构造函数,你可以选择自己重写外部的构造函数。

image-20211211003711654

委托(Delegation)

可以简单理解为composition by reference,即通过指针/引用复合

image-20211211004452966

继承(Inheritance)

image-20211211005736189

继承就是:子类有父类的成分

image-20211211005216822

同时要注意:父类的析构函数必须是virtual的,否则会出现undefined behavior

继承中的虚函数

non-virtual函数:你不希望子类重新定义(override)它

virtual函数:你希望子类重新定义它

pure virtual函数:你希望子类一定要重新定义它,你对他没有默认定义

image-20211211010211587

举一个例子:

利用虚函数延缓了主要功能的其中一个动作。可以让我们在后期通过虚函数修改自己的功能/添加新功能。

image-20211211010747368

image-20211211012342931