数据结构与算法分析笔记(C++)_c++类、c++细节

1.4C++类
本书中提供了许多数据结构。所有的这些数据结构都是用来存储数据(通常是相同类型项的集合)的对象,并且提供处理这些集合的函数。在C++(或者其他编程语言)中,这通过使用类完成。本节讨论C++类。
1.4.1基本class语法
在C++中类由成员(member)构成。成员可以是数据,也可以是函数,其中函数称为成员函数(member function)。类中的每一个实例都是一个对象。每一个对象包含类中指定的数据成员(除非这些数据成员是static,否则这是一个可以暂时安全忽略的细节)。成员函数作用于对象,通常被称为方法(method)。
public的类成员可以被任何类中的任何方法访问。private的类成员仅可以被它所在类的方法访问。一般地,数据成员声明为private,这样可以禁止对该类内部细节的访问,而作为一般用途的方法则定义为public。这被称为信息隐藏(information hiding)。通过使用private数据成员,可以改变对象的内部代码而不影响程序里其他使用到这个对象的部分。这是因为对象的访问是通过public成员函数实现的,而该成员函数的可见性并没有改变。类的使用者并不需要知道类实现的内部细节。
在类中,所有的数据成员的默认属性都为private
构造函数是描述如何构建类的实例的方法。如果没有显式定义的构造函数,那么可以自动生成使用编程语言的默认值来初始化数据成员的构造函数。
1.4.2特别的构造函数语法和访问函数
1.默认参数 default parameter
默认值0意味着,如果没有确定的参数,那么就使用0。默认参数可以在任何函数中使用,但是最普遍的情况是用在构造函数中。
2.初始化列表
构造函数在其代码体之前使用初始化列表(initializer list)用来直接初始化数据成员。
在数据成员是具有复杂初始化过程的类 类型的时候,使用初始化列表代替代码体中的赋值语句可以节省很多时间。
某些情况下,这是很必要的。例如,如果一个数据成员是const的(意味着在对象被构造后就不能再改变),那么,数据成员的值就只能在初始化列表里进行初始化。另外,如果一个数据成员是不具有零参数的构造函数的类类型,那么,该数据成员也必须在初始化列表里进行初始化。
3.explicit(显式的)构造函数
所有的单参数的构造函数都必须是explicit的,以避免后台的类型转换。否则,一些宽松的规则将允许在没有显式类型转换操作的情况下进行类型转换。这种不希望发生的行为会破坏代码的可读性,并导致难以发现的错误。

C++拥有宽松的规则。通常,单参数构造函数定义了一个隐式类型转换(implicit type conversion),该转换创建了一个临时对象,从而使赋值(或函数参数)变成兼容的。
注意,临时对象的构造也可以通过使用单参数构造函数来实现。使用explicit意味着单参数构造函数不能用来创建隐式临时对象。
4.常量成员函数
只进行检测但不改变其对象的状态的成员函数称为访问函数(accessor)。改变其对象的状态的成员函数称为修改函数(mutator)(因为该函数修改了该对象的状态)。
默认情况下,所有的成员函数都是修改函数,要使成员函数成为访问函数必须在参数类型列表结尾的圆括号后加上关键字const。const可以具有很多不同的含义。函数声明在不同的情况下可以有三种方式使用const。只有跟在结尾圆括号后的const才表示一个访问函数。
在这里插入图片描述
1.4.3接口与实现的分离
在C++中,将类的接口与其实现分离是很常见的。接口列出了类及其成员(数据和函数),而实现提供了函数的具体实现。一些要点如下:
1.预处理命令
接口通常都放在以.h结尾的文件中。需要接口信息的源代码必须#include接口文件。
偶尔,一个复杂的项目中有包含其他文件的文件,这样在编译一个文件时就存在一个接口被读两次的危险,这是非法的。为避免这种情况,每个头文件在读类接口时都定义一个预处理器来定义一个符号,如图1-7的前两行所示。
符号名Intce11_H不应该再出现在其他文件中,通常该符号都是文件名。接口文件的第一行检测该符号是否是未定义的。
如果答案是肯定的,就接着处理文件,否则就不处理文件(跳到#endif),因为该文件已知是读过的了。
数据结构与算法分析笔记(C++)_c++类、c++细节_第1张图片
2.作用域运算符::
实现文件通常都是以.cpp、.cc或者.c结尾的,其中的成员函数都必须声明为类的一部分。
否则,函数就会被认为是全局的(导致无数的错误)。语法是ClassName::member。::称为作用域运算符。
3.签名必须精确匹配 function signature 翻译为函数签名或函数特征标,也就是函数的参数列表,是函数重载的关键,用于识别不同的同名函数
实现的成员函数的签名必须与类接口中列出的签名精确匹配。
注意,默认参数仅在接口中被定义,在实现中则被忽略。
4.如基本类型一样声明对象
数据结构与算法分析笔记(C++)_c++类、c++细节_第2张图片
1.4.4vector和string
vector意在替代带来无穷麻烦的C++内置数组。
使用C++内置数组的问题在于其行为与基本类对象不同。例如,内置数组不能使用=复制,也不能记忆其本身能存储多少项,并且其索引操作不能检查索引是否有效。内置字符串仅是字符数组,这并没怎么提高数组的功效。例如,不能正确地比较两个内置字符串。
在STL中,vector和string类将数组和字符串作为基本类来处理。vector有确定的大小。两个string对象可以用
和<等进行比较。vector和string都可以用=进行复制。如果可能,应该避免使用C++内置数组和字符串。

1.5c++细节
1.5.1指针
指针变量是用来存储其他对象的存储地址的变量。
1.声明
在这里插入图片描述
第3行是m的声明。*说明m是一个指针变量,可以用来指向一个Intce11对象。m的值是它所指向的对象的地址。m在这时没有初始化。在C++中,在使用指针前并不对指针是否初始化进行检查(但是,有些供货商的编译器进行附加的检查)。使用未初始化的指针通常会破坏程序,因为这些指针会导致对不存在的存储地址的访问。一般来说,合并第3行和第5行或者初始化m为NULL指针都是好主意。
2.动态对象创建
第5行动态创建了一个对象。在C++中,new返回指向新建对象的指针。在C++中有两种方式可以使用零参数构造函数来创建对象。下面的两种写法都是合法的:由于可能出现的函数声明的问题,一般采用第二种写法。
在这里插入图片描述
3.垃圾收集及delete
C++没有垃圾收集。当一个通过new来分配地址的对象不再引用时,就需要使用delete操作(通过指针)将其删除。否则,该对象所占的内存就不能释放(直到程序结束)。这被称为内存泄漏 (memory leak)。
一个重要的规则就是能用自动变量的时候就不用new。
4.指针的赋值和比较
在C++中指针变量的赋值和比较是基于指针变量的值,也就是说它所存储的地址。
如果两个指针变量指向同一个对象,那么它们就是相等的
5.通过指针访问对象的成员
如果指针变量指向类类型的对象,那么该对象的(可见的)成员就可以通过->操作符进行访问。
6.其他指针运算
大于小于:对地址高低比较
一个重要的操作符是取地址运算符(&)。该操作符返回对象所在的内存地址
1.5.2参数传递
包括C和Java在内的许多编程语言都是使用按值调用(call by value)来传递参数的,即将实参复制给形参。
C++有三种不同的方式来传递参数
在这里插入图片描述
这里,arr是vector类型的,使用按常量引用调用(call by constant reference)来传递。
n是int类型的,通过按值调用来传递。errorFlag是boo1类型的,使用引址调用(call by reference)来传递。
参数传递机制的选用可以通过以下两步的判断来决定:
(1)如果形参必须能够改变实参的值,那么就必须使用引址调用。
(2)否则,实参的值不能被形参改变。如果参数类型是简单类型,使用按值调用。否则,参数类型是类类型的,一般按常量引用调用来传递(class类型复制开销较大)。
参数传递选项总结如下:
(1)按值调用适用于不被函数更改的小对象。
(2)按常量引用调用适用于不被函数更改的大对象。
(3)引址调用适用于所有可以被函数更改的对象。
1.5.3返回值传递
对象的返回也可以是按值返回和按常量引用返回,偶尔也用到引址返回。多数情况下,不要使用引址返回。
使用按值返回总是很安全的。但是,如果返回的对象是类类型的,更好的办法是使用按常量引用返回以节省复制的开销。然而,这只在下面的情况下才可能:必须确保返回语句中的表达式在函数返回时依然有效。(不要返回局部变量)
1.5.4引用变量
引用变量和常量引用变量常用于参数传递。它们也可以用作局部变量或类的数据成员。
在这些情况下,变量名就是它所引用的对象名的同义词(很像在引址调用中,许多形参名都是实参名的同义词)。作为局部变量,它们避免了复制的成本,因此在对含有类类型集合的数据结构进行排序时非常有用。
数据结构与算法分析笔记(C++)_c++类、c++细节_第3张图片
第二种用法是为了重命名一个具有复杂表达式的对象而使用的局部引用变量
引用变量可以用作类数据成员。引用必须被初始化(为它们将要引用的对象)。
1.5.5三大函数:析构函数、复制构造函数和operator=
在C++中,伴随类的是已经写好的三个特殊函数,它们是析构函数、复制构造函数和operator=。在许多情况下,都可以采用编译器提供的默认操作。有些时候却不行。
1.析构函数
当一个对象超出其作用域或执行delete时,就调用析构函数。通常,析构函数的唯一任务就是释放使用对象时所占有的所有资源。这其中包括为每一个相应的news调用delete,以及关闭所有打开的文件等。默认操作是对每一个数据成员都使用析构函数。
2.复制构造函数
被初始化为相同类型对象的一个副本,这就是复制构造函数(copy constructor)。
正如前面提及的,使用按值调用传递(而不是通过&或constant&)的对象无论如何都应该尽量少用。
通过值(而不是通过&或const&)返回对象。(深拷贝时需要重写复制构造函数)
3.operator=
当=应用于两个已经构造的对象时,就调用复制赋值运算符operator= (同样有深/浅拷贝的问题)
4.默认值带来的问题
主要问题出现在其数据成员是指针的类。
默认的析构函数不对指针进行任何操作(一个好理由就是释放这个指针就必须删除自身)。而且,复制构造函数和operator=都不复制指针所指向的对象,而是简单地复制指针的值。这样一来,就得到了两个类实例,它们包含的指针都指向了同一个对象。这被称为是浅复制(shallow copy)。一般,我们期望得到的是对整个对象进行克隆的深复制(deep copy)。于是,当一个类含有的数据成员为指针并且深复制很重要的时候,一般的做法就是必须实现析构函数、operator=和复制构造函数。(重写三大函数)
5.当默认值不可用时
最常见的默认值不可用的情况是,数据成员是指针类型的,并且被指对象通过某些对象成员函数(例如构造函数)来分配地址。
我们应用三大函数来解决这些问题。一般来说,如果析构函数是释放内存所必需的,那么复制赋值和复制构造函数的默认值就不适用。(即用到new则默认的三大函数需要被重写)
如果类所包含的成员函数不能复制自身,那么operator=的默认值就不可用。后续的内容里我们会看到一些这方面的例子。
1.5.6C风格的数组和字符串
在这里插入图片描述
arr1事实上是一个指向足够存储10个int的内存的指针,而不是基本类数组类型的。
在上面的定义中,在编译的时候数组的大小必须是已知的。10不可以用变量代替。如果数组的大小未知,就必须显式声明一个指针,并且用new[]来分配内存。例如,
在这里插入图片描述
释放
在这里插入图片描述
内置的C风格的字符串是当作字符数组来实现的。特殊的终止符\o用以标识字符串逻辑上的结束,以避免传递字符串的长度值。字符串可以通过strcpy来复制,也可以通过strcmp来比较。字符串的长度可以通过strlen来确定。每个字符都可以通过数组索引操作来访问。
这些字符串有数组所具有的所有问题,包括困难的内存管理问题。譬如当字符串进行复制时,总是假设目标数组的空间是足够大的。如果空间不够大的话,常会因为没有存储字符串终止符的空间而导致调试的极大困难。
标准的vector类和string类在实现的时候隐藏了内置的C风格的数组和指针的操作。
使用vector和string几乎总是较好的选择,但是,当使用同时为C和C++设计的库例程时也可能必须用到C风格的数组和字符串。偶尔(但是极少)为优化程序的运行速度,也会在代码中用到一小部分这些C风格的数组和字符串。

你可能感兴趣的:(数据结构与算法分析C++,c++,数据结构,编程语言)