适应群体:
本文将会从其他语言过渡到C++,适合有C语言基础和Java、Python等其他语言基础的同学快速学习以及上手。
参考书籍:
- 《C++程序设计教程》
- 《C++Primer》
前言:
C++是一门面向对象的语言,是基于C语言的扩展,通过提升程序的复用性提高了编程的效率。C++类的设计体现了对象的思想,多态和继承的设计则是基于对象的扩展。本文将会从其他语言过渡到C++。
一、本文使用经典的Josephus问题解释面向对象的思想,使用四个例子:
1、过程化的实现
2、结构化的实现
3、面向对象类的实现
4、面向对象模板类的实现
二、C++的数据类型以及语法,基础的知识点将会全部跳过。讲一讲C++的结构、数组、指针和引用(区分于其他面向对象语言的基础)
第二篇文章讲:
三、类的构建(讲类的构造、变量作用域、静态与友元函数、继承、多态)
四、运算符重载和模板
一、什么是面向对象语言?
人多人至今为止不知道面向对象的作用和意义,下面将会从工程的角度分析面向对象是怎么来的,他的作用和意义是什么。
我认为现在编程思路可以分为以下三种:
- 过程化编程
- 结构化编程
- 面向对象编程
语言只是实现编程思路的工具而已,使用对应的语言可以更加高效的完成工作目标。
下面将会讲使用不同编程思路实现Josephus问题方式。Josephus问题的代码我会以包的形式给出,我这里只描述实现方式。语法问题后面再说,理解思路最重要。
下面描述一下Josephus(约瑟夫环)问题:一堆小孩都有编号,编号是1,2,3 …n ,这群小孩(n个)按照1-n的顺序围坐一圈,从第1开始数,每数到第m个,该小孩就要离开此圈,这样依次下来,直到圈中只剩下最后一只小孩,则该小孩为大王。
1.过程化编程
首先我们可以将小孩放入一个数组内,用数组里面的值标识小孩的剔除操作。那么我们需要进行(n-1)次剔除操作。
那么编程的难点在哪呢?当我们跳过(m-1)的小孩过后,下一次需要进行数组越界的判定。这里可以使用循环来做(循环n-1次),那么我们需要一个值记录当前的小孩数和一个值记录当前位置,然后加入if判定就可以得到结果了。代码如下过过就好(interval=m):
//处理获胜前的小孩
while(1){
//在圈中数interval个小孩
for(int j=0; j<interval; ){
i=(i+1)%num; //对下标加1求模
if(a[i]!=0) //如果该元素的小孩在圈中,则承认数数有效
j++;
}
if(k==num) break; //该小孩是最后一个(胜利者)吗?
cout <<a[i] <<","; //输出离开的小孩之编号
a[i]=0; //标识该小孩已离开
k++; //准备处理下一个圈中小孩
}
//break语句跳转到此
cout <<"\nNo." <<a[i] <<" boy've won.\n"; //输出胜利者
可以看到有很多过程化语句(包括while、for、switch等)。
2.结构化编程
然而上一段代码是可以抽象的,我们可以使用循环链表来轻易的实现,这样更符合逻辑。剔除操作就是删除链表上的一个元素。并对数据和行为作出抽象。代码如下过过就好:
struct Jose{ //小孩结构
int code; //存放小孩编号
Jose* next; //用于指向下一个小孩结点
};
int n; //小孩数
int begin; //开始位置
int m; //数小孩间隔
Jose* pivot; //链表哨兵
Jose* pCur; //当前结点指针
int assign(); //赋初值,返回1:成功,0:失败
void initial(Jose* pBoys); //初始化环链表
void count(int m); //数m个小孩
void process(); //处理获胜者决出之前的所有小孩
void main()
{
if(!assign()){
cout <<"The program failed.\n";
return;
}
Jose* pJose=new Jose[n]; //分配结构数组
initial(pJose); //初始化结构数组
count(begin); //转到开始位置
process(); //处理获胜者决出之前的所有小孩
cout <<"\nthe winner is " <<pCur->code <<endl;
delete[]pJose; //返还结构数组给堆空间
}
可以看出代码的可读性提高了,但是,会有下面两点影响:
- 每个函数都需要用到一些全局变量,这使得函数的独立性大卫降低,函数本身的黑盒特性就遭到了破坏。
- 对于不懂数据结构的“用户”看到接口实在是气吐血:“我只想搭积木,你给我搞这些?”。最理想的应该是“用户”没必要看到我们的数据操作、直接接收到结果就行了
当然我们可以使用不同的文件提升独立性,但是本质上没有解决这种问题。数据的独立性与封装,“用户”只对行为做操控。
3.面向对象编程
根据上面的问题于是我们就可以进一步的抽象一下,抽象就是面向对象的基石,将链表单独实现为Ring,由Jose负责特定的逻辑(如:间隔多少个删除链表的元素,循环处理链表等等)。
struct Boy{} //链表元素
struct Ring{} //循环链表操作(在单独的文件下定义)
struct Jose{} //对孩子链表的一些特定逻辑(在单独的文件下定义)
void main()
{
Jose jose; //建立Josephus问题对象
jose.Initial();
jose.GetWinner();
cin.get();
}
但是还不够!!
假如有两个相同的问题要处理,那么我们就需要把定义的全局变量和行为(函数指针)全部放到一个结构体中,结构体里面的行为的传参是本结构体,可以访问本结构体的全部参数(有一点类的this指针的味道了),类的存在简化了这一操作(定义和赋值函数指针,数据的安全性等等)。自然而然就过渡到类了:
struct Boy{} //链表元素
class Ring{} //循环链表操作(在单独的文件下定义)
cladd Jose{} //对孩子链表的一些特定逻辑(在单独的文件下定义)
但是还不够!!
假如现在新增了一个男孩的元素、一个女孩的元素需要操作我们的对象如何组织呢?于是继承就来了(在Boy的基础上加性别或者其他信息)。
Ring怎么处理不同链表(男孩和女孩)元素呢?于是多态就来了(非必要时把你当成父类来看)。
Ring怎么处理不同链表(男孩、女孩、猴子、汽水)元素呢?于是模板就来了,来啥我就处理啥、返回啥。
二、数据类型以及语法
1.数据类型:
基础数据类型没啥好讲的,注意数据类型占用的字节数。
非基础数据类型中类后面单独再讲。需要注意的是数组、指针、引用、结构。
变量的生命周期可以归纳为:声明(定义)、初始化、使用、释放。我大体会以这四步来讲解数据类型。
2.数组
- 初始化
- 挨个元素初始化//a[i] = 0;
- 定义时初始化
- 可以省略表示元素数的常量N;
- C++中初始化值不能省略
- 全局数组需要在主函数运行之前初始化完成;
- 数组:int a[N] = {1,2,3,4,5};
- 字符数组:
- 需要结束符'\0'
- 可以省略“{}”
- 挨个“,”初始化;//char a[N] = 'a','b','c','d','\0';
- 使用字面量初始化;//char a[N] = “abcd”;//字面量自带结束符
- 使用
- 数组不是指针,可以从数组获取数组的地址和数组的长度等。将数组作为参数传给函数时,实际传的数组的地址。
- 可以使用memset()清空数组
- 通过下标访问元素;//a[1] = 1;
3.指针
- 使用
- 可以通过下标访问
- 可以通过加减改变指向位置,以一个类型为单位移动。
- malloc与new申请堆内存后用指针进行管理
- 常量指针:const int * p = &N;指向的内容不可变
- 指针常量:int * const p = &a;指向不可变
- 指针函数:int * fun(int a);//不能返回局部作用域的数据
- 函数指针:int (* func)(char a);//int为返回值类型,(* func)为指针,后面为参数列表
- 可以用typedef简化类型名:typedef int (* FUNC)(char a);FUNC funp;
4.引用
- 定义与初始化:int a = 1;int& b = a;
- 引用运算符只是在声明时使用,放在类型名的后面。
- 使用
- 相当于为变量起了一个别名,操作引用变量等于操作变量本身。
- 作为函数参数时使用const限定引用:表示不能对变量不能做为左值。
5.结构
- 使用
- 访问指针型结构成员“->”