C++入门——面向对象

面向对象

面向对象与面向过程,即对象

  1. 面向对象把数据即对数据的操作方法放在一起,作为一个相互依存的整体,即对象。对同类对象抽象出其共性,即类,类中的大多数数据都只能被本类的方法进行处理。类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。抽象流程由用户在使用中决定

  2. 面向过程是一个以事件为中心的开发方法,就是自顶向下顺序执行,逐步求精,其程序结构是按功能划分为若干个基本模块,这些模块形成一个树状结构,各模块之间的关系也比较简单,在功能上相互独立,每一模块内部都是由顺序、选择和循环三种基本结构组成的,其模块化实现的基本方法是使用子程序,而程序流程在写程序时就已经决定

具体地,二者的区别如下:

  1. 出发点不同。面向对象方法是用符合常规思维方式来处理客观世界的问题,强调把问题域的要领直接映射到对象及对象之间的接口上。而面向过程强调的是过程的抽象化与模块化,它是以过程为中心或处理客观世界问题的

  2. 层次逻辑关系不同。面向对象方法是用计算机逻辑来模拟客观世界中的物理存在,以对象的集合类作为处理问题的基本单位,尽可能地使计算机世界向客观世界靠拢,以使问题的处理更清晰更直接。面向对象方法是用类的层次结构来体现类之间的继承和发展。而面向过程方法处理问题的基本单位是能清晰准确地表达过程的模块,用模块的层次结构概括模块或模块间的关系与功能,把客观世界的问题抽象成计算机可以处理的过程

  3. 数据处理方式与控制程序方式不同。面向对象方法将数据与对应的代码封装成一个整体,原则上其他对象不能直接修改其数据,即对象的修改只能由自身的成员函数完成。控制程序方式上是通过“事件驱动”来激活和运行程序。而面向过程方法是直接通过程序来处理数据,处理完毕后即可显示处理结果。在控制程序方式上是按照设计调用或返回程序,不能自由导航,各模块之间存在着控制与被控制、调用与被调用的关系

  4. 分析设计与编码转换方式不同。面向对象方法贯穿软件生命周期的分析、设计及编码之间,是一种平滑过程,从分析到设计再到编码采用一致性模型表示,即实现的是一种无缝连接。而面向过程方法强调分析、设计及编码之间按规则进行转换,贯穿软件生命周期的分析、设计及编码之间,实现的是一种有缝的连接

面向对象的基本特征

面向对象方法首先对需求进行合理分层,然后构建相对独立的业务模块,最后通过整合各模块,达到高内聚、低耦合的效果,从而满足客户要求。具体而言,它有3个基本特征:封装、继承和多态

封装

封装是指将客观事物抽象成类,每个类对自身的数据和方法进行保护。类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。C++中,类是一种封装手段,采用类来描述客观上事物的过程就是封装,本质上是对客观事物的抽象

继承

继承是指可以使用现有的类的所有功能,而不需要重新编写原来的类,它的目的是为了进行代码复用和支持多态。它一般有3种形式:实现继承、可视继承、接口继承。其中,实现继承是指使用基类的属性和方法无需额外编码的能力;可视继承是指子窗体使用父窗体的外观和实现代码;接口继承仅使用属性和方法,实现滞后到子类实现。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式

多态

多态是指同一个实体同时具有多种形式,它主要体现在类的继承体系中,它是将父对象设置成为和一个或更多的它的子对象相等的技术,赋值以后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单地说,就是允许将子类对象的指针赋值给父类类型的指针,在运行时根据具体指向对象的类型来调用对应类

深拷贝与浅拷贝

如果一个类拥有资源(堆或者是其他系统资源),当这个类的对象发生复制过程时,资源重新分配,这个过程就是深拷贝;反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。浅拷贝资源后,释放资源时会产生资源归属不清的情况

友元

为了使非成员函数可以访问类的成员,可以将成员都定义为public,但这破坏了信息隐藏的特性。而且,对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查等都需要时间开销,从而影响程序的运行效率。友元正好解决这一问题

友元一般定义在类的外部,但它需要在类体内进行说明,为了与该类的成员加以区别,说明时在前面加关键字friend。需要注意的是,友元函数不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。成员函数和非成员函数的最大区别在于,成员函数可以是虚的,而非成员函数不行

使用有友元函数时,需要注意以下问题:

  1. 必须在类的说明中说明友元函数,说明时以关键字friend开头,后跟友元函数的函数原型,友元函数的说明可以出现在类的任何地方,包括private和public部分

  2. 友元函数不是类的成员函数,所以友元函数的实现与普通函数一样,在实现时不需要用“::”指示属于哪个类

  3. 友元函数不能直接访问类的成员,只能访问对象成员

  4. 友元函数可以访问对象的私有成员,但普通函数不行

  5. 调用友元函数时,在实际参数中需要指出要访问的对象

  6. 类与类之间的友元关系不能继承

拷贝构造函数与赋值运算符的区别

拷贝构造函数是一种特殊的构造函数,用来完成一些基于同一类的其他对象的构建及初始化工作。具体而言,拷贝构造函数有如下特点:

  1. 该函数名与类同名,因为它也是一种构造函数,并且该函数不指定返回类型

  2. 该函数只有一个参数,而且是对这个类的对象的引用

  3. 每个类都必须有一个拷贝构造函数

  4. 如果程序员没有显式地定义一个拷贝构造函数,那么C++编译器会自动生成一个缺省的拷贝构造函数

  5. 拷贝构造函数的目的是建立一个新的对象实体,所以一定要保证新创建的对象有独立的内存空间,而不是与先前的对象共用,最常见的是实现深拷贝

而赋值操作符则不一样,它只能被已存在的对象调用,它给已存在的对象赋予一个新的值,显然该对象原来就有值

C++中默认会产生哪些成员函数

C++中,空类默认会产生以下6个函数:默认构造函数、拷贝构造函数、析构函数、赋值运算符重载函数、取址运算符重载函数、const取址运算符重载函数

class Empty
{
public:
	Empty(); //默认构造函数
	Empty(const Empty&); //默认构造函数
	~Empty(); //析构函数
	Empty& operator=(const Empty&); //赋值运算符
	Empty* operator&(); //取址运算符
	const Empty* operator&() const; //取址运算符const
};

基类的构造函数/析构函数能否被派生类继承

不能。派生类中需要声明自己的构造函数。设计派生类的构造函数时,不仅考虑派生类所增加的数据成员的初始化,也要考虑基类的数据成员的初始化。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,需要调用基类构造函数完成

派生类需要自行声明析构函数。声明方法与一般类的析构函数相同,不需要显式地调用基类的析构函数,系统会自动隐式调用。需要注意的是,析构函数的调用次序与构造函数相反

初始化列表和构造函数初始化的区别

初始化列表一般如下:

Object::Object(int _x, int_y):x(_x), y(_y){}

构造函数初始化一般通过构造函数实现:

Object::Object(int _x, int _y)
{
	x = _x;
	y = _y;
}

上面的构造函数(使用初始化列表的构造函数)显式地初始化类的成员,而没使用初始化列表的构造函数是对类成员的赋值

初始化列表和赋值对内置类型的成员没有大的区别,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的。对非内置类型(类中的成员变量是一个对象)来讲,初始化列表有更好的性能,因为类类型的数据成员对象在进入函数体前已经构造完成,然后调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,这通过调用一个赋值操作符才能完成。为了避免两次构造,推荐使用类构造函数初始化列表

但很多场合必须使用带有初始化列表的构造函数。例如,成员类型是没有默认构造函数的类,若没有提供显式初始化时,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。再如,const成员或引用类型的成员,因为const对象或引用类型只能初始化,不能对它们赋值

具体而言,在C++中,只能用初始化列表,而不能用赋值的情况一般有3种:

  1. 当类中含有const(常量)、reference(引用)成员变量时,只能初始化,不能对它们进行赋值。常量不能被赋值,只能被初始化,所以必须在初始化列表中完成,C++的引用也一定要要初始化,所以必须在初始化列表中完成

  2. 派生类在构造函数中要对自身成员初始化,也要对继承过来的基类成员进行初始化,当基类没有默认构造函数的时候,通过在派生类的构造函数初始化列表中调用基类的构造函数实现

  3. 如果成员类型是没有默认构造函数的类,也只能使用初始化列表。若没有提供显式初始化时,则编译器隐式使用成员类型的默认构造函数,此时编译器尝试使用默认构造函数将会失败

类的成员变量的初始化顺序

在C++中,类的成员变量的初始化顺序只与变量在类中的声明顺序有关,与在构造函数中的初始化列表的顺序无关。而且静态成员变量先于实例变量,父类成员变量先于子类成员变量,父类构造函数先于子类构造函数

C++能设计实现一个不能被继承的类吗

C++不同于Java,Java中被final关键字修饰的类不能被继承。C++能实现不被继承的类,但需要自己实现

为了使类不被继承,最好的办法是使子类不能构造父类的部分,此时子类就无法实例化整个子类。在C++中,子类的构造函数会自动调用父类的构造函数,子类的析构函数也会自动调用父类的析构函数,所以只要把类的构造函数和析构函数都声明为private,那么当一个类试图从它那继承,必然会由于试图调用构造函数、析构函数而导致编译错误。此时该类不能被继承

但private的构造函数与析构函数无法得到该类的实例。此时可以通过定义静态来创建和释放类的实例。这也是经典的单例设计模式的实现原理。程序示例如下:

class FinalClass1
{
public:
	static FinalClass1* GetInstance()
	{
		return new FinalClass1();
	}
	static void DeleteInstance(FinalClass1* pInstance)
	{
		delete pInstance;
	}
private:
	FinalClass1(){}
	~FinalClass1(){}
};

在上例中,FinalClass1类是不能被继承的,但是通过该方法得到的实例都位于堆上,需要程序员手动释放。考虑到这一局限,设计如下一个类:

template <typename T> class MakeFinal
{
	friend T;
private:
	MakeFinal(){}
	~MakeFinal(){}
};

class FinalClass2 : virtual public MakeFinal<FinalClass2>
{
public:
	FinalClass2(){}
	~FinalClass2(){}
};

上例中的FinalClass2类使用起来与一般的类没有任何区别,既可以在栈上创建实例,也可以在堆上创建实例。而MakeFinal的构造函数和析构函数都是私有的,由于类FinalClass2是它的友元函数,因此在FinalClass2中调用MakeFinal的构造函数和析构函数也不会造成编译错误

对于FinalClass2类而言,当继承一个类并创建它的实例时,会出现编译错误。程序示例如下:

class Try : public FinalClass2
{
public:
	Try(){}
	~Try(){}
};
Try temp;

由于类FinalClass2类是从类MakeFinal虚继承过来的,在调用Try的构造函数时,会直接跳过FinalClass2,而直接调用MakeFinal的构造函数。但由于类Try不是MakeFinal的友元,因此不能调用私有的构造函数。所以,试图从FinalClass2继承的类,一旦实例化,都会导致编译错误,因此FinalClass2不能被继承

如何得知对象是否构造成功

这里的“构造”不单指对象本身的内存,而是指在建立对象时做的初始化操作(如打开文件、连接数据库等)

因为构造函数没有返回值,所以通知对象的构造失败的唯一方法就是在构造函数中抛出异常。构造函数中抛出异常将导致对象的析构函数不被执行,当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构

构造函数为什么没有返回值

构造函数与普通函数有很多不同的地方,构造函数在对象不存在的时候可以被调用,而普通函数只有在对象被创建出来后才可以被调用;其次,构造函数是被编译器来调用的,程序程序员不能显式地调用构造函数,而普通函数则可以被显式地调用

对象的创建可以分为两个步骤:

  1. 内存的分配
  2. 内存的初始化

对于内存分配,由于编译器知道类的详细构造,因此编译器可以完成内存的分配,而不需要构造函数的参与。但是,对于内存的初始化,它是与用户想实现的功能相关的,由于编译器没有这个上下文,所以内存的初始化主要由构造函数完成。

构造函数完全没必要有返回值,如果有返回值,会给编程带来歧义,例如:

class Object
{
public:
	bool Object(){...};
	...
};

SomeFun(Object obj){...}
SomeFun(bool flag){...}

当执行SomeFun(Object());函数调用的时候,此时调用的是SomeFun(Object obj),还是SomeFunn(bool flag)产生了歧义。无返回值的构造函数可以消除这个歧义

你可能感兴趣的:(C++,c++,面向对象,构造函数,析构函数,继承)