0%

对象和类

OOP特性:

  • 抽象
  • 封装和数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

接口分离: 1.提供类的声明 2.提供类成员函数

析构函数只有在构造函数用了new分配内存,才需要出来delete释放内存。否则无需工作。

const放在函数括号后:即为const成员函数,作用指不修改调用对象。

this指针的引入: 用来指向调用当前成员函数的对象(this作为隐藏参数传递给方法)

类静态成员变量: static const int Len = 30; 只能是整型或枚举的静态常量

类结合操作符重载

成为多态的重要一部分,隐藏了内部操作,强调了抽象的实质意义。
C++操作符重载要点:

  1. 重载后的操作符至少有一个操作数是用户定义的类型
  2. 使用操作符不能违反该操作符原有的句法规则
  3. 无法定义新的操作符
  4. 不能重载sizeof . .* :: ?: typeid *_cast

友元包含函数,类和成员函数

为何需要? 在为类重载二元操作符时,需要用到友元关系,方便使用。
存在的主要目的是作为类扩展接口的组成部分。
e.g Time乘以double可以用成员函数重载,但double乘以Time时不能,除非要求用户不能如此调用。否则应该引入友元函数重载。
一个常用的友元重载则是 cout << Time,而非用成员函数的 Time<< cout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 前一种成员函数重载 Time*double
Time Time::operator*(double mul) const
{
Time result;
long totalminutes = hours * mul * 60 + minutes * mul;
result.hours = totalminutes /60;
result.minutes = totalminutes %60;
return result;
}

// 后一种友元重载 double*Time
// 第一步:原型放入类的声明之中
friend Time operator*(double m, const Time & t);
// 第二步:定义编写
Time operator*(double mul, const Time & t)
{
Time result;
long totalminutes = t.hours * mul * 60 + t.minutes * mul;
result.hours = totalminutes /60;
result.minutes = totalminutes %60;
return result;
}

为了cout <<连续可输出,友元声明如下:

1
2
3
4
5
ostream & operator<< (ostream & os, const Time & t)
{
os << t.hours << " hours" << t.minutes<< " minutes";
return os;
}

接受单一参数的构造函数为类的类型转换提供了蓝图(blueprint)

蓝图是一个有意思的词语,后续多态也会继续接触到,是一个隐性类型表征;在类的类型转换上,需要尤其注意编译器二义性转换的问题。
警告:谨慎地使用隐式转换函数。explicit定义类的构造函数,则相关对象的类型转换需要显式调用,不能隐式转换。

类声明描述了如何分配内存,但并不执行分配内存

static int num;的初始化是在类声明之外,int className::num=0;

1
2
3
4
5
6
7
StringBad sailor = sports;

//等价于
StringBad sailor = StringBad(sports);

// 则原型为
StringBad (const StringBad &); //由于不知道更新类静态变量,导致计数出问题

隐式成员函数

C++自动提供下列成员函数

  • 默认构造函数
  • 复制构造函数
  • 赋值操作符
  • 默认析构函数
  • 地址操作符
    析构用了delete的类,所有对象生成的构造函数都应该使用new,否则会引起浅复制析构的错误。绝对避免试图删除已经删除的数据的行为!

书中的解决方案:使用deep copy,每个对象有自己的数据,而不是引用。
增加复制构造函数和赋值操作符,使类正确管理对象使用的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 拷贝函数深复制
StringBad::StringBad(const StringBad & st)
{
num_strings++;
len = st.len;
str = new char[len+1];
std::strcpy(str, st.str); // 复制构造,深复制而非隐式浅复制
}

// 赋值深复制:并非创建对象,而是对已有对象操作
StringBad & StringBad::operator=(const StringBad & st)
{
if(this == &st) // assigned to itself
return *this;
delete [] str; // free old
len = st.len;
str = new char [len+1];
strcpy(str, st.str);
return *this;
}

适配数组指针的释放语法

1
2
3
4
5
6
7
8
9
10
11
char words[15]="bad idea";
char * p1 = words;
char * p2 = new char;
char * p3;
// delete p1,p2,p3; suitable way

// insuitable way, undefined
delete [] p1;
delete [] p2;
delete [] p3;

new 对应 delete, delete[] 对应new []

初始化列表

执行在构造函数之前,因此可用于对const常量进行赋值,对声明为引用的类成员也类似。

  • 只能用于构造函数
  • 必须以此初始化非静态const数据成员
  • 必须以此初始化引用数据成员

继承is-a

用virtual虚函数以及动态指针来实现多态(dynamic binding动态编译,需额外开销),派生可自动向基类类型转换,称为向上强制转换。反之则不可,需显式转换。
引出C++指导原则之一:不要为不使用的特性付出代价

虚函数工作原理

编译器处理虚函数会增加一个隐藏成员指向该函数的地址,若派生重定义了虚函数,则该指针指向新的函数地址。
多态在内存和执行带来一定的成本:1.每个对象因存储地址而增大 2.编译器要为每个类创建虚函数地址表(数组) 3.每个函数调用需要额外查找表中的地址
重载的虚基函数在派生实现时改动需要全部一起改动,称为类型协变

应当把所有派生重新定义的函数再基类设置为虚函数,如果强制需重新定义则=0成纯虚函数

public/protected/private继承

派生公共继承关系是is-a,继承了基类的接口;其他两种是has-a关系,继承了成员成为私有,只可在声明内部使用;

构造函数,析构函数,=号,友元不能自动继承,需重新声明并实现。
protect继承则派生声明不能直接访问基类私有成员,通过public方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//单例模式
class TheOnlyInstance
{
public:
static TheOnlyInstance * GetTheOnlyInstance()
{
static TheOnlyInstance obj;
return &obj;
}

protect:
TheOnlyInstance(){}
private:
// other data
};
// 调用时 TheOnlyInstance * p = TheOnlyInstance::GetTheOnlyInstance();

传对象函数尽量用引用,避免构造和析构的开销;可以将派生对象用等号赋给基类对象,但相反则需提前明确定义。传引用可明确派生对象的类型,否则值引用可能会被编译器自动类型转换,发生意想不到的事情。也可使用dynamic_cast<const baseDMA &>(hs)的方式强制类型转换。