感谢侯捷老师的悉心讲授的课程,让我在对很多东西上有了更深层次的认识。
我呢,是一个非计算机专业毕业的本科生,毕业后带着对程序感兴趣的后知后觉开始学习编程,也不是抱着以工作的目的导向去的,学的比较杂,也缺乏系统性。算算日子,距离第一行java代码已经过去两年有余了,对于飞CS的我来说,坚持到今天也算不易。但是用了这么就的“面向对象编程”,但自己其实不能太说清其本质到底为何物。这也算是我正规划的第一步吧,再次感谢Boolan网和侯捷老师。那么我也就来简单的分享一下我所学到的知识吧,毕竟非专业出身,如果其中有错误的地方,希望大家能够指出来,谢谢大家。
Class是怎么来的
对于面向对象语言来说可能很重要的任务就是和class打交道吧(至少我接触的java、c++、python都是和class打交道的),但是在C语言中却没有见到过这个东西,最多也就有struct而已。
其实class对于认识世界来说是更加一致的,比如我们通常会把生物划分为动物、植物、微生物,把动物又划分为哺乳类、两栖类等等。我们在认识世界的时候往往会把具体的事物,比如猫、狗、人等抽象看待,找出其共性,再把不同点加入到他们自己的属性中去,就形成了“界门纲目科属种”这样的生物学划分规律。所以,也就是说,类和类之间应该有关系(比如人和猫都属于哺乳类动物),那么这些关系之间会有一些想通的属性(比如,人和猫都喝奶长大,都会运动等等)。但,不同种类也具有特殊的属性,比如猫有毛,人就没有等等。
而软件也应该对现实事物的一种抽象表现形式的描述,那么类就可以很好的把各种属性进行隔离并描述不同类之间的关系。比如,人和猫都具备喝奶的属性,但猫具备丰富的毛发,而人却不具备这个属性。因此,C++中使用class来隔离部分数据,将不同的数据分隔开来。同样,针对不同的属性,也就应该具有针对这个属性的响应操作方法(成员函数),比如,猫浓密毛发这个属性,那么他就具备“舔毛”的这个操作毛发的方法(函数),人没有浓密的毛发则就不需要这个函数了。
因此,C++相较于C增加重要的概念class,用这个概念来让程序能够使用更加抽象的方式(属性+操作属性的函数(方法))来描述这个世界。
什么是面向对象(Object Oriented)
其实无论Java还是C++的编程过程中,最重要的需要设计各种各样的class,对于Java来说,C++的class要复杂一些,C++ class可以分为“带有指针成员变量的的class”和“不带指针变量的class”。而,对于每次设计出来的单一class来说,这就属于一种“基于对象(Object Based)”的编程。
如果再拿刚才我说的人和猫来看,对于我们需要抽象一个class来描述这个类别的时候,我们这个过程,其实就是基于对象(Object Based)的过程,比如为了描述猫咪,而建立了一个class猫咪。如果为了实现某一个复杂的过程,我们往往设计一个class是不够的。比如我们养猫的过程来说吧,在构造这个人和猫的系统的时候,显然需要class人,也需要class猫咪,这两种class之间从抽象的角度来看是不是有一些共通之处呢?如果说吃的不一样我们在这里先不谈,那么喝的水总归是一样的吧,呼吸的空气总归也是一样的吧,如果为了描述人和猫分别独立设计两个class,也许并没有这个必要,所以,再进一步抽象的时候,我们也许会得到(这只是为了描述这个过程,不一定非常贴切,谁让我养猫呢,低头抬头看到的全是猫)一个class 动物,这时候把水和空气作为一种通用属性放在动物类中,而进一步设计class猫和class人的关系时,就只需要通过class动物这个类来描述他们俩之间的关系了比如对我来说一定class人里面少不了铲猫砂,撸猫等等这一类特有的函数(方法);class猫中存在捣乱、卖萌这类特有的函数了。而喝水、吃饭、呼吸、睡觉(猫一般一天能睡十八九个小时,真羡慕他们,有吃有喝有睡,而我必须挣钱养他们。。。。好像说着说着class人中又多了一个方法。。。)这类方法(函数)虽然动物类就有,但是人和猫毕竟都还是有区别,这时候又可以通过覆盖这些方法来表示共性中的不同点。
因此,面向对象相较于面向过程来说,就是进一步对数据和操作方法的抽象,通过进一步的抽象来描述多个class之间的关系的抽象方法。
C++程序的基本形式
说了那么多关于基于对象和面向对象的故事,那么C++程序到底是由什么东西构成呢
-
C++程序可以分类两种:.cpp(C++文件)和.h(头文件)
而头文件往往也分为两部分,一部分为class的声明(Classes Declaration),另外一部分则为标准库(Standard Library),其中标准库部分里面包含了大量的算法,可以让我们在设计类的时候不需要重复造轮子。
-
那么.cpp和.h如何关联在一起呢?
是通过#include来引入标准库或这头文件。其中如果是引入标准库的部分,使用的是<>来引入,系统中的标准库文件,比如#include <iostream.h>。如果引入的是C语言的标准库,可以使用cname或者name.h的方式引入,比如#include <stdio.h>或这#include <cstdio>;
如果是自己所编写的头文件需要使用""来引入进来,比如#include "complex.h",一般情况下,这个表示的是和cpp文件在同一个目录下的.h文件,如果.h文件在cpp文件目录的某个文件夹中,需要使用#include "/dir/xxx.h"来引入了。
对于C和C++的输出有什么区别呢?
C++
#include <iostream>
int main()
{
int i = 7;
std::cout << "i= " << i << endl;
return 0;
}
- C语言
#include <stdio.h>
int main()
{
int i = 7;
printf("i=%d \n", i);
return 0;
}
-
头文件编写的防御式声明
#ifndef __COMPLEX__ #define __COMPLEX__ //防御式的声明 ......... #endif
-
目的:
- 可以让使用者更加自由的include这个头文件
- 防止同一个程序中重复的导入这个头文件
-
头文件的布局
#ifndef __COMPLEX__ #define __COMPLEX__ //前置声明(forward declarations) class ostream; class complex; complex& __doapl(complex* ths, const complex& r); //类-声明(class declarations) class complex { .... }; //类-定义 complex::function .... #endif
-
-
以complex类(复数类)举例说明类-声明的定义
class complex //class head { //class body public: //访问级别access level为public的部分可以被外部直接访问的部分 complex (double r = 0, double i = 0) : re(r), im(i) { } //complex () : re(0), im(0) { } //构造函数的重载,但是由于有参数的函数有默认值,所以不能共同存在 complex& operator += (const complex&); //有些函数在body之外定义 double real() const { return re; } //有些函数再次直接定义 double imag() const { return im; } void imag(double i){ im = i; } //函数的重载 private: //访问级别access level为private的部分只能被class内部和friend的函数直接访问 double re, im; //数据应该放在private里面,以达到封装的效果 friend complex& __doapl(complex* ths, const complex& r); }
-
inline(内联)函数
- 在类本体内所定义的函数
例如(之前定义的class):
double imag() const { return im; }
- 不在本体内定义的函数,使用inline关键字修饰的函数(是否能成为inline函数具体情况需要由编译器来决定,属于对编译器的建议)
例如:inline double imag(const complex& x) { return x.imag(); }
- 在类本体内所定义的函数
-
构造函数(Constructor)
complex (double r = 0, double i = 0) //默认实参 : re(r), im(i) //初始列,构造函数专有的设置参数初值的方法,效率比直接赋值要高 { } complex () : re(0), im(0) { } //构造函数的重载
构造函数会在创建对象的时候被自动调用
函数名和类型相同,并且无返回值
如果创建对象时没有传递参数,会调用默认参数的构造函数
使用初始列设置初值的效率高于在函数体中赋值的过程
构造函数可以重载(overloading)
-
构造函数可以放在private中,作为私有的,可以作为singleton的设计模式(内存中只有一个对象)
//singleton class A{ public: static A& getInstance(); setup(){.........} private: A(); A(const A& rhs); .... }; A& A::getInstance() { static A a; return a; }
-
常量成员函数
double real() const {return re;}
- 对于不改变数据的函数,应该添加const关键字
- 如果不添加const关键字,则使用者创建一个常量c1
const complex c1(2, 1)
会报错
-
参数传递:pass by value vs. pass by reference (to const)
- pass by value:将数据打包整体传传递过去(如果对象比较大,会是效率降低)
- pass by reference:引用在底部为指针,传递效率相当于传递指针
- 尽量传递引用,而不要传递值
- 传递引用被修改,则原始值也被修改
- 如果为了提高效率,但不希望原始内容被修改,则应该使用const修饰
complex& operator += (const complex& x)
-
返回值传递:return by value vs. return by reference (to const)
返回值尽量使用return by reference
如果返回的结果实在函数中创建的,则不能以reference返回,因为随着函数结束而结束,不能以reference返回,否则会获取已被销毁的对象
-
传递着无需知道接收者是以reference的形式接收
inline complex& __doapl(complex* ths, const complex& r) { .... return *ths;//返回值要求为reference,则此处返回实际内容即可 //传递着无需知道接收者是以reference的形式接收 }
-
友元(friend)
对于被friend修饰的函数,可以直接取得private中的成员
-
相同class的各个object互为友元(friend)
//定义 class complex { public: complex(double r = 0, double i = 0):re(r), im(i){} int func (const complex& param){return param.re + param.im} private: double re, im; }
{ //调用 complex c1(2, 1); complex c2; c2.func(c1); //可以直接访问c1中的成员(相同class的不同对象互为友元) }
-
操作符重载(
c2 += c1;
)- 成员函数类型
- 二元操作符,具有两个操作数,系统会将操作符作用在左边身上,如果左边操作数有定义,系统能够找到对应的操作符重载
- 对于成员函数类型的操作符重载,编译器在编译时会为该函数自动添加this指针,该this指针就相当于操作符左侧的操作数,所以在定义函数时并不需要传入两个参数,只需要传入右侧的操作数即可
-
例如
c2 += c1;//操作符作用在c2上
-
定义方法
inline complex& complex::operator += (const complex& r) { return __doapl(this, r); //返回值的设计要求主要是为了满足链式调用的情况 //返回值可以满足这样的要求:c3 += c2 += c1; //过程是c2 += c1先运算,然后产生返回值,再和c3运算 } //实际上函数会被编译器添加this指针 //complex::operator+= (this, const complex& r) //其中c2 += c1;调用时,相当于将c2以this的身份传入,c1以另外的参数传入
-
- 非成员函数类型(无this)
- 由于是非成员函数,则编译器并不能帮助函数添加this指针,则则参数列表中,需要两个形式参数,分别来表示操作符左右两侧的操作数
inline complex operator + (const complex& x, const complex& y) { return complex(real(x) + real(y), imag(x) + imag(y)); } inline complex operator + (const complex& x, double y) { return complex(real(x) + y, imag(x); } inline complex operator + (double x, const complex& y) { return complex(x + real(y), imag(y)); //此处生成了新的复数对象,输入内部变量,所以返回值不能是reference } //为了方便例如7+c1的情况,所以不讲该函数设计为成员函数,而是使用全局函数来定义
- 成员函数类型