面向对象程序设计
程序是完成一定功能的一系列有序指令的集合。我想,对于这个定义,汇编程序员的理解是最深的。汇编语言用助记符代替机器指令,用地址符号或标号代替指令或操作数的地址。特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。汇编语言的抽象层次不高,大部分时候我们还是需要以机器的思维来考虑问题,如需要考虑寻址方式,并且代码和数据之间的界限不明确。现在我们已经很少使用汇编语言了,大部分时候都是使用高级语言。
程序 = 数据结构 + 算法,这是结构化程序设计的核心思想。它将程序定义为处理数据的一系列过程。这种设计方法的着眼点是面向过程的,特点是数据与程序分离,即数据与数据处理分离。结构化程序设计的基本思想是采用自顶向下、逐步细化的设计方法和单入单出的控制结构。其理念是将大型程序分解成小型、便于管理的任务。如果其中的一项仍然过大,则将它分解为更小的任务。这一过程将一直持续下去,知道将程序划分为小型的,易于编写的模块。结构化程序设计为处理复杂问题提供了有力手段,但到80年代末,这种设计方法逐渐暴露出以下缺陷:程序难以管理、数据修改存在问题、程序的可重用性差。面向过程程序设计缺点的根源在于数据与数据处理分离。
程序 = 对象 + 对象 + ... ... + 对象;对象 = 数据结构 + 算法。这是面向对象程序设计的主要思想。面向对象程序设计模拟自然界认识和处理事务的方法,将数据和对数据的操作方法放在一起,形成一个相对独立的整体——对象,同类对象还可抽象出共性,形成类。一个类中的数据通常只能通过本类提供的方法进行处理,这些方法成为该类与外部的接口。对象之间通过消息进行通讯。结构化设计方法应用的是过程抽象。过程抽象是将问题域中具有明确功能定义的操作抽取出来,并将其作为一个实体看待。面向对象设计方法应用的是数据抽象。数据抽象是将描述客体的属性和行为绑定在一起,实现统一的抽象,从而达到对现实世界客体的真正模拟。
面向对象的基本特征
-
封装。封装是指按照信息屏蔽的原则,把对象的属性和操作结合在一起,构成一个独立的对象。通过限制对属性和操作的访问权限,可以将属性“隐藏”在对象内部,对外提供一定的接口,在对象之外只能通过接口对对象进行操作。封装性增加了对象的独立性,从而保证了数据的可靠性。外部对象不能直接操作对象的属性,只能使用对象提供的服务。
-
继承。继承表达了对象的一般与特殊的关系。特殊类的对象具有一般类的全部属性和服务。当定义了一个类之后,又需定义一个新类,这个新类与原来的类相比,只是增加或修改了部分属性和操作,这时可以用原来的类派生出新类,新类中只需描述自己所特有的属性和操作。继承性大大简化了对问题的描述,大大提高了程序的可重用性,从而提高了程序设计、修改、扩充的效率。
-
多态。多态性,即同一个消息被不同对象接收时,产生不同结果,即实现同一接口,不同方法。一般类中定义的属性和服务,在特殊类中不改变其名字,但通过各自不同的实现后,可以具有不同的数据类型或具有不同的行为。继承和多态性组合,可以生成很多相似但又独一无二的对象。继承性使得这些对象可以共享许多相似特性,而多态又使同一个操作对不同对象产生不同表现形式。
面向对象编程方法的特性
- 程序设计的重点在数据而不是函数。
- 程序由对象组成,建立对象的目的不是为了完成某一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
- 对象之间通过相互协作来完成功能。
- 函数与相关的数据紧密结合。数据可以被隐蔽。
- 很容易扩充新的数据和函数。
类的定义
类是一种用户自定义类型,类中可以定义数据和函数,数据可以是基本数据类型,也可以是类类型,函数可以带缺省参数,用inline修饰,函数还可以重载。其声明形式如下。
class 类名称
{
public:
//公有成员(外部接口)
private:
//私有成员
protected:
//保护成员
};
- 公有成员。在关键字public后面声明,它们是类与外部的接口,任何外部函数都可以访问公有类型数据和函数。
- 私有成员。在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。
- 保护成员。在关键字protected后面声明,只允许本类和派生类中的函数访问,而类外部的任何函数都不能访问。
在C++中,struct也可以定义类,它和class关键字的区别是,在未指定访问权限时,class默认是私有的,而struct默认是公有的。
this指针和static关键字
先看这样一段代码:
#include <iostream>
using namespace std;
class A{
public:
int val;
void print(){
cout<<"val = "<<val<<endl;
}
};
int main(int argc, char const *argv[])
{
A a1,a2;
a1.val = 1;
a2.val = 2;
a1.print();
a2.print();
return 0;
}
仔细想想这段代码,就会发现一个问题,print怎么知道是哪个对象调用了它,它的参数里面没有指明对象啊?其实因为绝大多数的情况下,我们使用类中的函数都需要使用对象的值,为了简单起见,在函数调用的时候,会自动传入一个对象的指针,也就是说,成员函数有一个隐含的附加形参,即指向该对象的指针,这个隐含的形参叫做this指针。使用this指针保证了每个对象可以拥有不同的数据成员,但处理这些成员的代码可以被所有对象共享。
普通成员函数必须使用对象进行调用,并且由于this指针的缘故,处理的数据都是对象自己的。现在有这样一个问题,如果在一个类中,需要有所有对象共享的数据,怎么办?这就引入了static关键字,使用static关键字声明的数据是独立于对象之外的,并且只有一份,是所有对象共享的。同时,static不仅可以声明数据,还可以声明函数,static声明的函数是独立于对象的,也就是说,使用类作用域符就可以调用,但是,同时,static声明的函数不能使用非static的数据成员。
前向声明
C++中类必须先定义,才能够实例化。两个类需要相互引用形成一个“环形”引用时,无法先定义使用。这时候需要用到前向声明。前向声明的类不能实例化,因为前面声明仅仅是声明了类的名称,仅仅知道类的名称,编译器无法构造出具体的对象。
嵌套类
- 从作用域的角度看,嵌套类被隐藏在外围类之中,该类名只能在外围类中使用。如果在外围类的作用域使用该类名时,需要加名字限定。
- 嵌套类中的成员函数可以在它的类体外定义。
- 嵌套类的成员函数对外围类的成员没有访问权,反之亦然。
- 嵌套类仅仅只是语法上的嵌入。
局部类
- 类也可以定义在函数体内,这样的类被称为局部类。局部类只在定义它的局部域内可见。
- 局部类的成员函数必须被定义在类体中。
- 局部类中不能有静态成员。
构造函数
在C语言中,是没有构造函数的,但是C语言提供了一种相对简单的构造方法。
struct Student{
char *name;
int age;
};
Student st = {name,age};
但对于C++来说,初始化要复杂一些,所以C++提供了构造函数来对对象进行初始化。构造函数是特殊的成员函数,创建类类型的新对象,系统自动会调用构造函数,也就是说,构造函数是为了保证对象的每个数据成员都被正确初始化。构造函数有如下的一些特点。
- 函数名和类名完全相同。
- 不能定义构造函数的类型(返回类型),也不能使用void。
- 通常情况下构造函数应声明为公有函数,否则它不能像其他成员函数那样被显式地调用。
- 构造函数被声明为私有有特殊的用途。
- 构造函数可以有任意类型和任意个数的参数,一个类可以有多个构造函数(重载)。
C++提供了初始化列表,可以方便的对类中的数据成员进行初始化,它的一个例子如下。
A(int val1, int val2):val1_(val1),val2_(val2){}
在很多类中,初始化和赋值的区别事关底层效率问题:前者字节初始化数据成员,后者先初始化再赋值。此外,一些数据成员必须被初始化,只能使用初始化列表进行初始化,如引用和const常量。
构造函数可以重载,根据参数的不同,有如下一些特殊的构造函数。
默认构造函数(无参构造函数)
没有参数的构造函数称为默认构造函数,或者无参构造函数,如果程序中未声明其他构造函数,则系统自动产生出一个默认构造函数。值得注意的是,在《C++primer》中,说定义在函数外的变量会执行默认初始化,但实际的效果是,在有的编译器中并不会执行。看下面的代码。
class A{
public:
int x;
int y = 0;
};
按照《C++primer》的说法,A中的x会被设置为0,但实际上,g++中x还是一个随机值,但y的值会被正确初始化为0。
转换构造函数及explicit关键字
转换构造函数,也就是单个参数的构造函数,之所以叫转换构造函数,是因为它可以将其它类型转换为该类类型。类的构造函数只有一个参数是非常危险的,因为编译器可以使用这种构造函数把参数的类型隐式转换为类类型。如果想要隐式类型转换,可以使用explicit关键字,explicit是只提供给类的构造函数使用的关键字,编译器不会把声明为explicit的构造函数用于隐式转换,它只能在程序代码中显示创建对象。
还有一点值得注意的地方,就是赋值和初始化有一些不同,初始化的时候,等号是特殊处理的,不能看做是等号运算符。请看下面的代码。
class A{
public:
int val_;
A(int val):val_(val){cout<<"A() "<<val_<<endl;}
~A(){cout<<"~A() "<<val_<<endl;}
A& operator=(const A& a){
cout<<"operator="<<endl;
val_ = a.val_;
return *this;
}
};
int main(int argc, char const *argv[])
{
/* 如果用explicit声明A,后面两个语句都无法通过编译 */
A a1 = 10; /* 初始化,不会调用等号运算符 */
a1 = 20; /* 先把20转化为一个类A,然后使用等号运算符进行赋值 */
return 0;
}
拷贝构造函数及深浅拷贝
只有一个参数并且参数为该类对象的引用的构造函数称为拷贝构造函数。拷贝构造函数主要作用是使用一个已经存在的对象来初始化一个新的同一类型的对象。如果类中没有说明拷贝构造函数,则系统自动生成一个缺省复制构造函数,作为该类的公有成员。拷贝函数主要在下列两个场景中调用:
- 当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用。这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中。
- 当函数的返回值是类对象,函数执行完成返回调用者时使用。理由也是要建立一个临时对象中,再返回调用者。为什么不直接用要返回的局部对象呢?因为局部对象在离开建立它的函数时就消亡了,不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中。所谓return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象。如果返回的是变量,处理过程类似,只是不调用构造函数。
默认的拷贝构造函数是逐值拷贝,在有些情况下是不适用的。看下面的代码。
#include <iostream>
#include <stdlib.h>
using namespace std;
class A{
public:
int *val_;
A(){
val_ = (int *)malloc(sizeof(int));
*val_ = 1;
}
};
int main(int argc, char const *argv[])
{
A a1;
A a2(a1);
a2.val_[0] = 0;
cout<<a1.val_[0]<<endl;
return 0;
}
因为是逐值拷贝,所以a2和a1的val_指向了同一块内存,则显然是有点不合理的。因为改变了a2的值,a1的值就会跟着改变。
析构函数
析构函数和构造函数恰好相反,析构函数是对象销毁是调用的。析构函数有如下一些特点。
- 函数名和类名相似(前面多了一个字符“~”)。
- 没有返回类型。
- 没有参数。
- 析构函数不能被重载。
- 如果没有定义析构函数,编译器会自动生成一个默认析构函数。
- 默认析构函数是一个空函数。
new和delete
new和delete是C++新增的关键字,和malloc和free的功能相似,只不过new和delete除了分配空间外,还会调用对象的构造析构函数。new即可以创建对象也可以创建数组,而delete只能删除对象,如果要删除数组,需要使用delete[]。
网友评论