C++ 中的代码重用

第十四章讲的是C++中的代码重用,代码重用是C++的主要目标之一,因此,通过多种继承、模板来实现。

包含

公有继承是is-a关系,也就是说is a type of,可认为是并列关系。但两个类之间不光有并列关系,还是包含关系:has-a关系。那么就又有了包含,私有继承,保护继承。

包含很简单,就是在类的声明中添加一个类的对象作为数据成员。

私有继承

私有继承基类的公有成员和保护成员都将成为派生类的私有成员。这将意味着基类方法将不会成为派生对象的公有接口的一部分,但可以在派生类的成员函数中使用它。

访问基类

通过初始化列表直接调用基类析构函数初始化基类对象,而对于访问基类对象则可以使用强制转换。当然,访问基类对象是在他的可见作用域,派生类的内部实现的。

保护继承

保护继承,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变为私有方法;使用保护继承时,基类的公有方法在第二代中将变成裱糊的,因此第三代派生类可以使用他们。

using重新定义访问权限

对于保护的方法,可以使用using来在public重新定义对外访问权限,定义方法比如:

using std::valarray::min;

来开放min函数的对外权限。

多重继承

多重继承可以让一个类从多个基类继承。

如果多个基类有共同祖先,那么新继承的类中可能有多个相同部分(共同祖先),当然,他们并存而且可以分别访问(通过,对基类的作用域解析运算符),但是大多数情况是不需要的,因此,可以在继承的时候添加virtual,使用虚基类,那么,对于多个虚基类的共同祖先,只会产生一个。

如果是虚基类,在构造函数就不应该用初始化列表调用基类,而应该调用基类的祖先。在虚基类中,这是必须的,但是非虚基类,这是非法的。

因为多重继承经常产生二义性的问题,因此,善用作用域解析运算符。

类模板

C++的类模板为生成通用的类声明提供了一种更好的方法。模板提供参数化类型,既能够将类型名作为参数传递给接收方来建立类或函数。

使用方法,在类的声明和函数的定义(如果是类外定义的话)前加上:

template

类模板可以做基类,可以用作组件类,可以做其他模板的类型参数,可以递归嵌套使用。

模板的具体化

和函数模板一样,可以隐式实例化,显式实例化,显式具体化。甚至可以部分具体化(只支出几个而不是全部参数)。

成员模板

类模板可以作为类的成员或者模板类的成员但在template语句是注意嵌套关系

模板用作参数

模板包含类型参数和非类型参数。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL。

模板和友元

模板类声明也可以有友元,模板的友元分3类:

  1. 非模板友元,即和模板无关的友元
  2. 约束模板友元,即友元的类型取决于类被实例化的类型
  3. 非约束模板友元,即友元的所有具体化都是类的一个具体化的友元

C++11别名

之前的别名都是通过typedef实现,C++11允许使用using,比如:

using LL = long long;

和typedef相比,不仅可读性更强,而且对于模板的重命名更方便,比如:

template
using arrtype = std::array;


所有的这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制他们,这样可以简化编程工作,提高程序可靠性!

2014/11/26 01:16 am posted in  C++

C++ 的类继承

面向对象编程的主要目的之一是提供可重用的代码,C++类提供了更高层次的重用性,比如类继承。类有三种继承方式,公用,私有,保护。这一章只讲公有继承。

派生一个类

使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

  • 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
  • 派生类对象可以使用基类的方法(派生类继承了基类的接口)
  • 派生类需要自己的构造函数
  • 派生类可以根据需要添加额外的数据成员和成员函数

构造函数

构造函数必须给新成员和继承的成员提供数据,但是派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问,换句话说,要构造派生类,需要调用基类的构造函数。

  • 首先创建基类对象
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员

释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

指针

基类指针可以指向派生类对象,但相反却不可以。基类指针指向派生类对象是向上强制转换,而派生类指针指向基类对象是向下强制转换。如果是强制转换的话倒也可以编译,但是是有风险的。因为基类的方法派生类都有,但派生类的方法,基类不一定有。这是C++的一个保护手段。

也正是因为基类的指针和引用可以指向派生类,导致了,原来为基类准备的函数,派生类也可以调用。很符合代码的重用性。

多态公有继承

如果派生类想改变基类中已经存在的类方法,那么只需要再重新定义一个就好,但是这样出现了一个问题,因为基类指针是可以指向派生类的,那么如果指针或引用调用类方法的时候,默认是基类的类方法,尽管两个方法的实现不同,毕竟是基类指针,哪怕指向的是派生类函数。

为了解决这个问题,可以再基类的方法的声明前加上virtual,表示是虚方法,那么,当指针和引用调用方法的时候会判断到底是基类方法还是派生类方法。

如果重新定义基类方法时却需要调用基类方法肿么办?为了防止陷入递归死循环,应该使用作用域解析符。

还要注意,只要派生类重新定义了类方法,基类中的所有类方法都不能用了,包括基类中的函数重载。

因为基类可能使用动态内存,派生类也可能使用,因此,最好在基类中使用虚析构函数,保证派生类释放时正确。不是采用基类析构函数释放的。

访问控制:protected

protected和private的区别只有在基类派生的类中 才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来书,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

最好对数据成员私有而不是保护,部分方法可以保护。

抽象基类

如果两个或多个类有共同点,而又有不同点,那么便可以声明一个只含有共同点的抽象基类,然后其他各个类都作为这个基类的派生,只添加不同点。

要声明抽象基类,需要让它包含纯虚函数,所谓纯虚函数,就是虚函数的声明后面加上个=0.

纯虚函数可以没有定义,但也可以有定义,他的定义只用在派生类没有定义的情况。纯虚函数用来定义派生类的通用接口。

抽象基类不被允许声明对象,只是作为一个基类使用,只要包含纯虚函数便被认为是抽象基类。

强制转换

我也不知道为什么把强制转换单独拿出来总结,感觉挺重要的,或者说挺容易错的。

因为基类和派生类的关系很微妙,派生类在一定程度上讲,对基类的方法挺兼容,但是对象友元函数,运算符重载等一些特殊情况,很容易照成二义性甚至是因为类型判断失误导致递归性的死循环,因此善用强制转换,尤其是在基类和派生类类型模糊的时候。多想想,多写点,累不死。^^

2014/11/25 01:16 am posted in  C++

C++ 类和动态内存分配

动态内存和类

动态内存如此灵活,在类中也十分常用。

如果在构造函数的时候使用new,那么无论是构造,赋值,复制,析构都需要注意

如果在类中有个static静态成员变量,便可以在类外用作用域解析运算符直接赋值,但是要指出变量类型,如:

int A::num = 0;

如果静态成员是const或者枚举,则可以在类声明初始化。

在复制,赋值过程中,可能会用到临时变量,如果在类中存在动态内存,那么给一个对象赋值时,是一个临时变量给这个对象赋值,过程:

  1. 创建临时变量,其中包括以后指向动态内存空间的指针
  2. 将临时变量成员依次赋值给要赋值的对象
  3. 释放临时变量,包括释放那段动态内存

很明显,这样形成了一个野指针。而这个复制过程被成为浅赋值。也就是由编译器默认复制构造函数实现的。但是,对于使用动态内存的类来说,这样明显是有问题了。因此,要使用深度复制。

深度复制就是使用复制构造函数和重载赋值运算符。

静态类成员函数

可以将成员函数声明为静态的(函数声明包含关键字static)

  1. 不能通过对象调用静态成员函数
  2. 静态成员函数甚至不能使用this指针
  3. 如果静态成员函数是在共有部分声明的,则可以使用类名和作用域解析运算符来调用它
  4. 由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员
  5. 可以使用静态成员函数设置类级标记,以控制某些类接口的行为

在构造函数中使用new时应注意的事项

  1. 如果在构造函数中用了new,则应在析构函数中使用delete
  2. 如果使用new[],那应该使用delete[]
  3. 如果有多个构造函数,应该使用相同的new格式,因为只有一个析构函数,要保证兼容性
  4. 应该定义一个赋值构造函数,通过深度复制讲一个对象初始化为另一个对象(初始化,形参调用)
  5. 应该定义一个赋值运算符,通过深度复制将一个对象赋值给另一个对象

指针和对象之间的使用,和指针和结构体等的使用方式相同。善待指针!

为了防止隐式转换可以使用explicit关键字。

初始化列表

应该是一个星期前,韩祥波老师把我叫讲台上做一个关于初始化列表的题,不过到现在我才知道什么是初始化列表= =。

其实就是C++为const常量初始化的一种方式,只能用在构造函数上。

如果Queue类中有个叫qsize的常量,常量除了初始化是不允许被赋值的,那么就使用初始化列表进行初始化:

Queue (int qs):qsize(qs)
{
...
}

其他变量也可以用初始化列表进行初始化。其初始化的顺序和类声明中的顺序相同,和初始化列表的顺序无关。

C++11给diao,他可以在类声明中,直接初始化,甚至就是直接默认参数。


就像之前说的,就是在总结的时候浪费时间,那么我节约总结的时间来保证进度,那么代价就是总结不详细。但是有弊也有利,我现在很明确每一篇总结的重点,难点。因为写的都是重点难点。

2014/11/22 01:15 am posted in  C++

C++ 如何使用类

类就像服务器给客户开了一个玩笑,然后想尽千方百计的圆这个谎。

运算符重载

运算符重载就是C++中多态的另一种表现,目的是让类使用起来更自然。

使用方法,类内重载:

返回值 operator运算符(参数);

比如,对于类A:

A operator + (int n);

使用就可以

A tmp;
int n;
tmp = tmp + n;

但是n+tmp是错误的,因为类内重载说白了还是成员函数,运算符左面就是第一个参数,也就是对象本身,运算符右面就是重载运算符函数的参数。如果需要反过来,可以用类外重载(非成员函数重载),也就是通过友元函数。

以下10种运算符不允许重载(不允许重载的运算符):

. :成员运算符

?: :条件运算符

siezof::sizeof运算符

:: :作用域解析运算符

.* :成员指针运算符

typeid:一个RTTI运算符

const_cast:强制转换运算符

dynamic_cast:强制转换运算符

teinterpret_cast:强制转换运算符

static_cast:强制转换运算符

友元函数

友元有3种:友元函数,友元类,友元成员函数。

通过函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限,这里主要写友元函数。

为了解决上文的问题,其实大部分运算符都可以类外重载。以此来交换传参的顺序。

友元就是在成员函数最前面加个friend。

这样,这个函数就不是成员函数,就不能用成员运算符调用

虽然不是成员函数,但是他却又类作用域。

因为不是成员函数,所以编写函数定义的时候不需要加作用域限定符,不要再定义的时候加friend。

重载ostream & operator(ostream &os,A b)

{
os

当然,这个重载要作为A的友元才能方法A的私有成员数据。当然,如果A类有返回要打印的数据的函数,重载不一定必须是友元。换句话说,只要能打印出数据,是不是友元无所谓,我们重载operator TypdName();

注意:

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数

比如:

operator double();

使用起来也比较奇葩,比如对于类A:

A tmp = 1.2;
double tt = double(tmp);

这就是为什么说更像数据类型重载而不是强制转换了。

但是还要注意的是:没有返回类型不是没有return,要不然,强制转换之后,到底赋值赋的什么呢?还是靠return控制的,只是在声明和定义中不写罢了。

当然,转换函数有了,显式隐式转换都无所谓了,因为都可以实现。

最后一点,一定要注意好转换中的二义性,比如,定义了int和double转换函数,却给一个long赋值,会怎么样呢?

如果只有一个转换函数,那么语法没有错误,int或double对long都是合法的,但如果都存在,则编译器无法判断使用哪个重载,便报错,对于这种情况,要么就再定义一个long 的转换函数,要么直接使用强制转换。

 One more thing

如果转换函数和友元函数联合起来,效果是惊人的。一个可以控制类型的转换,一个可以进行运算等操作。

但是,过多的转换函数却很容易导致二义性,所以说,玩火谨防尿床。

但还是分析一下:

如果依赖隐式转换完成运算,程序更简短,定义的函数更少,出错几率也就小但是每次转换的时候都将调用转换构造函数,这增加时间和内存开销。反之,利用重载运算符,程序员需要完成的工作更多,但程序运行速度较快。

2014/11/21 01:14 am posted in  C++

C++ 的对象和类

C++通过改进C语言,使得应用更容易,最重要的OOP特性:

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

为了把他们组合在一起,C++所做的最重要的改进是提供了类。

未开发一个类并编写一个使用它的程序,通常,C++程序员将结构(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。

为了帮助识别类,常见但不通用的约定,将类名首字母大写。

访问控制

类中包含private,public和protected。

类设计尽可能将共有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据的隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。

数据隐藏不仅可以防止直接访问数据,还让开发者无需了解数据是如何被表示的。在对后期程序修改的时候,无需修改程序接口,这使程序维护起来更容易。

由于隐藏数据是OOP主要目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分。通常程序员使用私有成员函数来处理不属于公有接口的实现细节。

其实类和结构唯一区别就是结构默认的访问类型是public,而类为private。

实现类成员函数

除了类的描述(class部分),还需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数定义与常规函数非常相似,它们有函数头和函数体等等。但是:

  • 定义成员函数时,使用作用域解析运算符来表示函数所属的类
  • 类方法可以访问类的private组件

定义在类声明中的函数会自动生成内联函数,当然在类声明之外定义的成员函数,也可以使用inline使之成为内联函数。

所创建的每个新的对象都有自己的存储空间,用于存储其内部变量和成员。但同一个类的所有对象共享同一组类方法。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。

使用类

C++的目标是使得使用类与使用基本的内置类型(int等)尽可能相同。要创建类对象,可以声明类变量,也可以使用new对类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋值给另一个。C++提供了一些工具可用于初始化对象,让cin和cout识别对象,甚至在相似的类对象之间进行自动类型转换。

OOP程序员常依照客户/服务器模型进行讨论程序设计,客户是使用类的程序。类声明(包括类方法)构成了服务器,服务器的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的细节而不能修改端口。这样对服务器的修改不会客户的行为造成意外的影响。

类的构造函数和析构函数

构造函数

C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值付给他们的数据成员。更准确地说,C++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义。

构造函数名称与类名相同,注意好参数列表,可以重载。

注意参数名不要和private数据名相同(否则无法赋值),一般采取加前缀的方式。

使用构造函数一般有两种方式,一是显式使用(直接调用构造函数,用好参数),另一种是隐式使用。

class S
{
...
}

//显式使用
S a = S(...);

//隐式使用
s a(...);

//使用new
S *p = new S(...);

若未提供显式初始化值会调用默认构造函数,但不会初始化其成员。

如果没有自定义默认构造函数,编译器会提供一个没有任何操作的默认构造函数。

定义默认构造函数有两种,一是给已有构造函数的所有参数提供默认值;二是通过函数重载定义另一个没有参数的构造函数。

因为只能有一个默认构造函数,因此不要同时出现以上两种方式。用户自定义默认构造函数,一般是给所有成员初始化><。

隐式使用默认构造函数时不要使用括号,画色添足,成声明一个函数了。

析构函数

在构造函数创建对象后,程序负责跟踪该对象,直到过期为止。

对象过期之后,程序将调用死神函数析构函数。析构函数完成清理工作。其实只有在动态内存上,析构函数才有更实际的意义。

比如,同new开辟的内存,析构函数用delete来释放,其他情况,基本用不到析构函数= =,说的好不负责任。。。。

析构函数和构造函数差不多,只不过前面加了一个~。如果没有手动申明一个析构函数的话,编译器会自动加上析构函数,当然,里面什么都没有,如果自定义类没有涉及到动态内存的话,其实这样也挺好。手懒的表现。

什么时候调用析构函数是由编译器决定的,一般是和栈一样,先入后出。

C++11的初始化列表一样可以用在对象的初始化上,不过列表里的内容是某构造函数的参数列表。

对于类方法不会修改到被调用对象时,应该在函数后面添加const。const能用就用, 不只是安全!

this指针

因为在类方法中,终会遇到需要返回或者比较调用对象的信息,那么this指针应时而生,他就是一个指针,指向了当前的调用对象,用法和指针一样。

对象数组

C++目的就是使得程序员自定义构造的类可以和既有的数据类型使用,因此,可以用对象创建数组等稍微高级点的数据结构。对象数组的初始化方式有点类似结构体,但是花括号内是调用构造函数(显示调用)或者直接什么都不写,默认构造函数构造。(如果是C++11的话,可以直接用初始化列表,这样更像结构体数组了)。

类作用域

C++自从有了类,就有了一种特殊的作用域。类内的元素,作用域只在类内,也就是说只对类内可见。这就是为什么在定义成员函数时必须使用作用域解析运算符。

因为有了作用域,也就有了作用域为类的常量,如果想用一个const常量来控制数组的长度,这是行不通的,因为在类中声明的const常量要在编译时就要分配空间,而建立类的时候,只是描述这个类而没有分配空间,所以常量的值是未知的,则数组的长度也是未知的。

解决方法有两个,一个是用枚举,一个是用static const,全局常量。

抽象数据类型(ADT)

类很适合描述ADT,公有成员函数接口提供了ADT描述的服务,类的私有部分和类方法的代码提供了实现,这些实现对类的客户隐藏。

2014/11/20 01:14 am posted in  C++