数学知识的复习
在计算机科学中,所有的对数都是以2为底的,除非另有声明。
模运算 如果N整除A-B,那么久说A和B同余(congruent),记为A=B (modN)
证明数据结构分析中得结论两个最常见的方法是归纳法证明和反证法证明(偶尔也不得已用唬人法证明)
1 归纳法证明的两个标准的部分 第一步 证明基准情形(base case)第二步 归纳假设(inductive hypothesis)
这意味着假设定理对直到某个有限数K的所有情况都是成立的,然后使用这个假设证明定理对下一个值也是成立的。
2 反证法证明通过假设定理不成立,然后证明该假设导致某个已知的性质不成立,从而证明原假设是错误的。
递归的简单介绍
当一个函数用自身来定义时就成为递归(recursive)。c++允许函数递归的,但是必须记住,c++所做的仅仅是试图遵循递归的思想。不是所有的数学函数都能被有效地(或正确的)用c++的递归模拟来实现。
int f( int x)
{
if( x == 0)
return 0;
else
return 2*f(x-1) + x*x;
}
递归的基本法则:
(1)基准情形(base cases),必须总有某些基准的情形,他们不用递归就能求解。
(2)不断推进(making progress),对于那些要被递归求解的情形,递归调用必须总能能朝着一个基准情形推进。
(3)设计法则(design rules),假设所有的递归调用都能运行。
(4)合成效益法则(compound interest rules),在求解一个问题的同一个实例时,切勿在不同的递归调用中做重复性的工作。
C++类
在C++中类是由成员(member)构成,成员可以是数据,也可以是函数。在类中所有的数据成员的默认属性都是private
class IntCell
{
public:// 而作为一般用途的方法则定义为public。
IntCell() // 构造函数是描述如何构建类的实例的方法。 注意什么时候调用哪个构造函数。
{
storedValue = 0;
}
IntCell(int initialValue)
{
storedValue = initialValue;
}
int read()
{
return storedValue;
}
void write(int x)
{
storedValue = x;
}
private:// 一般地,成员变量声明为private,这样可以禁止对该类内部细节的访问。
int storedValue;
// 以上的定义法则成为信息隐藏(information hiding)
};
对上面的类进行修改
class IntCell
{
public:
explicit IntCell(int initialValue = 0) // 使用explicit意味着单参数构造函数不能用来创建隐式临时对象。
: storedValue(initialValue)// 初始化列表(initializer list)用来直接初始化数据成员。
{
}
// 只进行检测但不改变其对象的状态的成员函数成为访问函数(accessor)
// 默认情况下,所有的成员函数都是修改函数,要使成员函数成为访问函数必须在参数类型列表结尾后面加上关键字 const
// 如果成员函数标记为访问函数但在实现中又去改变数据成员的值,那么就会出现编译错误。
int read() const
{
return storedValue;
}
//改变其对象的状态的成员函数成为修改函数(mutator)
void write(int x)
{
storedValue = x;
}
private:
int storedValue;
};
接口和实现的分离
接口列出类及其成员(数据和函数), 实现提供了函数的具体实现。
头文件.h
一个复杂的项目中有包含其他文件的文件,这样在编译一个文件时就存在一个接口被读两次的危险,这是非法的,
为避免这种情况,每个头文件在读类接口时都定义一个预处理来定义一个符号。
#ifndef IntCell_H
#define IntCell_H
class IntCell
{
public:
explicit IntCell(int initialValue = 0);
int read() const;
void write(int x);
private:
int storedValue;
};
#endif
源文件 .cpp
#include "IntCell.h"
//其中的成员函数都必须声明为类的一部分,否则,函数就会被认为是全局的, :: 称为作用域运算符
IntCell::IntCell(int initialValue = 0) : storedValue(initialValue)
{
}
int IntCell::read() const
{
return storedValue;
}
void IntCell::write(int x)
{
storedValue = x;
}
#include<iostream>
#include "IntCell.h"
using namespace std;
int main()
{
IntCell m;
IntCell m1(6);
//IntCell m4(); 也是编译通不过。
//IntCell m2 = 7; 假如没有explicit声明的构造函数,那么这个编译就会通过。
m.write(3);
cout << "Cell contents: " << m.read() << endl;
return 0;
}
C++细节
指针变量是用来存储其他对象的存储地址的变量
int main()
{
IntCell *m;
// 声明m是一个指针,m的值是它所指向对象的地址,在c++中,在使用指针齐纳并不对指针是否初始化进行检查,但使用未初始化的指针通常会破坏程序
//因为这些指针会导致对不存在的存储地址的访问,所以一般来说是写成 IntCell *m = new IntCell(0);
// 在c++中指针变量的赋值与比较式基于指针变量的值,也就是说它所存储的地址
// 一个重要的操作符是取地址运算符(S)该操作运算符返回对象所在的内存地址。
m = new IntCell(0);
//在c++中new返回指向新建对象的指针。
m->write(3);
cout << "Cell contents:"<< m.read()<<endl;
delete m;
//当一个通过new来分配地址的对象不再引用时,就需要使用delete操作将其删除。
return 0;
}
double avg(const vector<int>& arr, int n, bool& errorFlag);
arr是vector<int>类型的,使用按常量引用调用(call by constant reference)来传递。
n int类型的,通过按值调用来传递,
errorFlag是bool类型的,使用地址调用(call by reference)来传递。
参数传递机制的选用可以通过以下两步来判断
(1)如果形参必须能够改变实参的值,那么就必须使用地址调用。
(2)否则,实参的值不能被形参改变,如果形参类型是简单类型,使用按值调用,否则,参数类型是类类型的,一般按常量引用调用来传递
参数传递选项总结如下:
1 按值调用使用于不被函数更改的小对象
2 按常量引用调用使用于不被函数更改的大对象
3 引址调用适用于所有可能被函数更改的对象。
对象的返回也可以按值返回和按常量引用返回,偶尔也用到引址返回
如果返回的类型是类类型,更好的办法是使用按常量引用返回来节省复制的开销。
引用变量和常量引用变量常用于参数传递,他们也可以用作局部变量或类的数据成员,在这种情况先,变量名就是它所引用的对象名的同义词。
三大函数:析构函数 复制构造函数和operator=在c++中,伴随类的是已经写好的三个特殊函数 析构函数 复制构造函数和operator=
1 析构函数
当每一个对象超出其作用域或执行delete时,就会调用析构函数,通常析构函数的唯一作用就是释放使用对象时所占有的内存
2 复制构造函数
有一种特殊的构造函数,用于构造新的对象,被初始化为相同类型对象的一个副本,这就是复制构造函数(copy constructor)
复制构造函数可以以如下实例进行调用
(1)声明的同时初始化
eg:IntCell B = C;
IntCell B(C);
而不是 B = C
(2)使用按值调用传递的对象无论如何都应该尽量少用。
(3)通过值返回对象。
默认情况下,复制构造函数通过将构造函数依次应用到每一个数据成员来实现。
3 operator= 复制赋值运算符 在默认情况下,operator=通过将其依次应用于每一个数据成员来实现。
默认值所带来的问题
默认的析构函数不对指针进行任何操作,而且,复制构造函数和operator都不复制指针所指向的对象,而是简单的复制指针的值。
对数组使用=的结果是复制两个指针的值,而不是整个数组。
如果数组的大小未知,就必须显式声明一个指针,并且用new[]来分配内存。
int *arr2 = new int [n];
然而由于内存是动态分配的,在某些地方就必须使用delete[]来释放
delete[] arr2;
模板
所谓类型无关,就是说这种算法的逻辑与存储在数组中的项的类型无关,相同的逻辑可以适用于整数,浮点数或者具有可比拟性的任何类型。
在C++用模板(template)来写类型无关的算法。
函数模板(function template)不是真正的函数,而是一个用以产生函数的公式。
template<typename Comparable>
const Comparable& findMax(const vector<comparable> &a)
{
int maxIndex = 0;
for(int i = 1; i < a.size(); i++)
{
if(a[maxIndex] < a[i])
maxIndex = i;
}
return a[maxIndex];
}
函数模板可以应需要而自动扩展,要注意的是随着每种类的类型的扩展,都会生成新的附加代码,在大项目中,这被称为代码膨胀(code bloat)
在决定参数传递和返回值传递时,模板实参可以使用任何类型类类型,所以,模板的实参应该定义为非基本数据类型,这也是采用常量引用的原因。
类模板
template<typename Object>
class MemoryCell
{
public:
explicit MemoryCell(const Object& initialValue = Object())
: storedValue(initialValue)
{
}
const Object & read () const;
{
return storedValue;
}
void write(const Object& x)
{
storedValue = x;
}
private:
Object storedValue;
};
默认参数是通过零参数构造函数来构造Object的结果。
MemoryCell不是类,而仅仅是类模板 MemorCell<int>和MemoryCell<string>是真实的类
Object Comparable和例子
Object和Comparable作为泛函类型来使用。Operator定义为含有一个零参数构造函数,一个operator和一个复制构造函数。
操作符重载(operator overloading)
一个如传递参数一样传递函数的巧妙的方法是:定义一个包含零个数据和一个成员函数的类,然后传递这个类的实例。