摘要 教程二主要涉及到以下内容:C++经典语法与应用、类的编写与应用、构造函数(Constructor)与析构函数(Destructor)、函数的重载(Overload)、类的继承(Inheritance)、函数覆盖(Override)、基类(Base Class)与派生类(Derived Class)的构造函数、析构函数调用的顺序,如何在派生类构造函数中向基类的构造函数传递参数,this指针,类型转换的内幕,虚拟函数与多态性,引用和指针变量的区别与共同处。VC工程的编译原理与过程,将工程中不同的类拆分到不同的文件中,每一个类由一个.h和.cpp文件共同完成,头文件重复定义问题的解决等。通过本次学习,可以为以后学习分析MFC应用程序的开发奠定良好基础。根据视频内容,设置本章目录如下:
1、C++及面向对象基础概念 1.1 C++的标准输入输出流 1.2 结构体(struct)与类(class)的区别 | 1.3 C++主要特点 1.4 类与对象的定义、区别及联系 1.5 面向过程(OP)与面向对象(OO) |
2、C++的特性 2.1 构造函数(Constructor) 2.2 析构函数(Destructor) 2.3 函数的重载(Overload) | 2.4 this指针 2.5 类的继承(Inheritance) 2.6 函数的覆盖(Override) 2.7 多态性(Polymorphism) |
3 引用和指针变量的内存模型 | 6 C++预编译指令防止类的重复定义 |
4 关于#include 双引号与尖括号的区别 | 7 VC++程序编译链接原理与过程 |
5 VC++代码的组织 | 8 附录(重载、继承、多态实例代码) |
关键词 构造函数;析构函数;函数重载;函数覆盖;继承;多态;Visual C++;
1、C++及面向对象基础概念
1.1 C++的标准输入输出流
C++中提供了一套输入输出流类的对象,它们是cin 、cout和cerr,对应c语言中的三个文件指针stdin、stdout、stderr,分别指向终端输入、终端输出和标准出错输出(也从终端输出)。
1.2 结构体(struct)与类(class)的区别
在C语言中,结构体不能包含函数,而在C++中可以。struct与class的区别主要在访问控制方面:在结构体中,所有的成员缺省情况下均为public,而对class来说,缺省情况下,所有的数据成员和成员函数都是私有的,不能够在外部进行访问。
1.3 C++主要特点
封装性(Encapsulation):把数据与操作数据的函数组织在一起,使程序结构更加紧凑,提高类内部数据的安全性。
继承性(Inheritance):增加了软件的可扩充性及代码重用性;
多态性(Polymorphism):使设计人员在设计程序时可以对问题进行更好的抽象,有利于代码的维护和可重用
1.4 类与对象的定义、区别及联系
1.4.1 专业版
类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起;
对象是具有类类型的变量。类是对象的抽象,对象是类的具体实例。
1.4.2 通俗版
木有神马比实例更能说明问题的了。比如学生,“它”拥有姓名、性别等属性,这就是一个学生类;
而对具体的某一个学生如奶茶妹,“她”有她自己的姓名和性别,那这就是一个对象了。这个例子也就对应了专业版的说法:类是对象的抽象,对象是类的具体实例。
1.5 面向过程(OP)与面向对象(OO)
1.5.1 专业版
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
1.5.2 通俗版
亲,五子棋肯定会的吧,面向过程的做法:开始游戏→黑子先走→绘制画面→判断输赢→轮到白子→绘制画面→判断输赢→返回第二步→输出最后结果。
而面向对象的做法如下:先抽取对象,包括行为相同的黑白双方,称为玩家对象,另外还有棋盘系统和规则对象(个人觉得这里的对象称为类更合适);然后玩家对象负责接受用户输入,并告知棋盘对象更新棋局,最后规则系统对棋局进行判定。
2、C++的特性
2.1 构造函数(Constructor)
①构造函数的函数名与类名相同,用于创建对象本身,无返回值,也不能用void来修饰;
②C++规定,每个类必须有一个构造函数,没有构造函数,就不能创建任何对象;
③如果一个类没有提供任何的构造函数,则C++提供一个默认的构造函数(由C++编译器提供),这个默认的构造函数是一个不带参数的构造函数,它只负责创建对象,而不做任何的初始化工作;
④只要一个类定义了一个构造函数,不管这个构造函数是否是带参数的构造函数,C++就不再提供默认的构造函数。也就是说,如果为一个类定义了一个带参数的构造函数,还想要无参数的构造函数,则必须自己定义。
2.2 析构函数(Destructor)
①当一个对象生命周期结束时,其所占有的内存空间就要被回收,这个工作就由析构函数来完成;
②析构函数是“反向”的构造函数,析构函数不允许有返回值,更重要的是析构函数不允许带参数,并且一个类中只能有一个析构函数;
③析构函数的作用正好与构造函数相反,对象超出其作用范围,对应的内存空间被系统收回或被程序用delete删除时,析构函数被调用;
④根据析构函数的这种特点,我们可以在构造函数中初始化对象的某些成员变量,给其分配内存空间(堆内存),在析构函数中释放对象运行期间所申请的资源。
2.3 函数的重载(Overload)
定义:两个或两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数进行最佳匹配,自动确定调用哪个函数,这就是函数的重载。
条件:函数的参数类型、参数个数不同,才能构成函数的重载。以下这两种情况无法构成函数的重载(编译器无法确定应该调用哪个函数):
①void output();与int output();
②void output(int a,int b=5);与void output(int a);
2.4 this指针
①this指针是一个隐含的指针,它是指向对象本身,代表了对象的地址(注:不是类);
②一个类所有的对象调用的成员函数都是同一代码段。那么成员函数又是怎么识别属于同一对象的数据成员呢?实际上,在对象调用pt.output(10,10)时,成员函数除了接受2个实参外,还接受到了一个对象s的地址。这个地址被一个隐含的形参this指针所获取,它等同于执行this=&pt。所有对数据成员的访问都隐含地被加上前缀this->。例如:x=0; 等价于 this->x=0.
2.5 类的继承(Inheritance)
①基类/父类、派生类/子类的概念;
②类的继承访问权限:
继承方式 | 基类private成员 | 基类protected成员 | 基类public成员 |
private | no access | private | private |
protected | no access | protected | protected |
public | no access | protected | public |
③在实例化一个对象时,父类的对象会想构造,然后再构造子类对象,而他们的析构顺序恰恰相反;
④在设计父类时,若只有一个带参数的构造函数,那么在子类当中,我们需按以下例子写子类的构造函数:Fish():Animal(400,300){}
⑤若在子类中定义了一个常量的数据成员如(private: const int a;),则可在④的基础上写成:Fish():Animal(400,300),a(1){}
⑥子类与父类的内存模型请参照视频01:15:25;
⑦关于类的继承的更多内容,视频中的讲解更容易理解,这里不再做更多记录。
2.6 函数的覆盖(Override)
①函数覆盖发生在父类与子类之间,其函数名、参数类型、返回值类型必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做函数覆盖。
②函数的覆盖是发生在父类与子类之间,而函数的重载是发生在一个类里的;
③若想在子类中调用父类的方法,可通过::作用域标识符来表示,如:Animal::breathe();
④实例化一个子类时,其内存布局包括父类对象内存和子类特有部分内存,this指针指向父类对象内存。因此this指针
2.7 多态性(Polymorphism)
多态性一词最早用于生物学,指同一种族的生物体具有相同的特性。在面向对象的程序设计理论中,多态性的定义是:同一操作作用于不同的类的实例,将产生不同的执行结果,即不同类的对象收到相同的消息时,得到不同的结果。
2.7.1 虚函数
定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数
语法:virtual 函数返回类型 函数名(参数表){函数体}
用途:虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
用法:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数
2.7.2 纯虚函数
使用虚函数,我们可以灵活的进行动态绑定,当然是以一定的开销为代价。如果父类的函数(方法)根本没有必要或者无法实现,完全要依赖子类去实现的话,可以把此函数(方法)设为virtual 函数名=0 我们把这样的函数(方法)称为纯虚函数。相应的类则称为抽象类。
2.7.3 静态联编
静态联编是指联编工作出现在编译连接阶段,这种联编又称早期联编,它解决了程序中的操作调用与执行该操作代码间的关系。
2.7.4 动态联编
编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进行联编工作被称为动态联编。
2.7.5 静态多态性
静态多态性是指定义在一个类或一个函数中的同名函数,它们根据参数表(类型以及个数)区别语义,并通过静态联编实现,例如,在一个类中定义的不同参数的构造函数。
2.7.6 动态多态性
动态多态性是指定义在一个类层次的不同类中的重载函数,它们一般具有相同的函数,因此要根据指针指向的对象所在类来区别语义,它通过动态联编实现。
3 引用和指针变量的内存模型
①引用就相当于变量的别名,且必须在定义的时候就初始化,而指针是保存变量地址的一个变量;
②指针变量本身需要有内存空间存放,而引用则不需要;
③引用通常用于函数传参,在有些时候比指针更直观更容易理解,如指针:change(&pA, &pB)与引用:change(a,b),用指针就难以判断是交换a与b的地址还是交换a的地址与b的地址.
4 关于#include 双引号与尖括号的区别
比如:“coridc.h” 和 #include<coridc.h>;这两者的区别就在于查找目录的顺序,双引号表示先在程序源文件所在目录查找,如果未找到则去系统默认目录查找,通常用于包含程序作者编写的头文件;尖括号表示只在系统默认目录或者括号内的路径查找,通常用于包含系统中自带的头文件。
5 VC++代码的组织
一般类的定义和函数原型的声明放到头文件中,把类中成员函数的实现放到一个源文件(.cpp)中。
6 C++预编译指令防止类的重复定义
#ifndef POINT_H_H
#define POINT_H_H
class Point{};
#endif
7 VC++程序编译链接原理与过程
假设项目HelloVC共有N个源文件(cpp文件),则其过程如下:
Step 1:预处理器对源文件中的预处理指令进行处理,在内存中输出N个翻译单元(临时文件);
Step 2:编译器接收预处理的输出,将源代码转化为包含了机器指令的N个目标文件(obj文件);
Step 3:链接器对三个目标文件及所使用的C/C++标准库函数(lib文件)进行链接,生成一个可执行的exe文件。
8 附录(重载、继承、多态实例代码)
#include <iostream>
using namespace std;
class Animal
{
public:
//定义一个带参构造函数,则编译器不再指定一个不带参的构造函数
Animal(int height, int weight)
{
cout<<”Animal construct”<<endl;
}
~Animal()
{
cout<<”Animal destruct”<<endl;
}
virtual void breathe()
{
cout<<”Animal breathe”<<endl;
}
//纯虚函数,在子类中必须实现基类的纯虚函数后才能实例化对象
//virtual void breathe() = 0;
};
class Fish : public Animal
{
public:
Fish():Animal(100,200),a(20)
{
cout<<”Fish construct”<<endl;
}
~Fish()
{
cout<<”Fish destruct”<<endl;
}
void breathe()
{
//Animal:breathe();
cout<<”Fish bubble”<<endl<<this->a<<endl;
}
private:
const int a;
};
void fn(Animal *pAnimal)
{
pAnimal->breathe();
}
int main()
{
Fish fish;//构造fish对象
//fish.breathe();
Animal *pAnimal;//构造了一个指向animal的指针
pAnimal = &fish;//将fish对象地址赋给指向animal的指针
fn(pAnimal);//传递指针参数
system(“pause”);
}