C++面向对象程序设计(基础版上)

目录

一、前言

二、基于对象

(一)头文件头部基础介绍

a.C++代码的基础形式

b.头文件中的防卫式声明

(二)头文件主体内容介绍一(以Complex类为例,没有指针的class)

a.数据

b.函数:

(三)头文件主体内容介绍二(以String类为例,有指针的class)

a.Big three

b.堆、栈和内存管理

c.扩展补充

四、面向对象

(一)类与类之间的三大基本关系

a.复合关系(compostion)

b.委托关系(delegation)

c.继承关系(inheritance)

(二)关系的组合

五、小小的总结

一、前言


        此为本人在复习和学习C++的一些课程总结,如有不准确地方还请指教,学习仅供参考。

       面向对象程序设计分为上下两次课程,在基础版上的课程中,主要的分类是基于对象(单个类实现的基础介绍)和面向对象(类与类之间的关系)。此外,本文章主讲C++语言,部分学习采用C++标准库的设计来介绍。

       (阅读前小Tip:在C++中分类主要是分为 不带指针的Class 和 带指针的Class。在标准库中对应的典型分类有 Complex类(复数类)和 String类,接下来的基于对象介绍也是按这两个类展开。)

二、基于对象


(一)头文件头部基础介绍

a.C++代码的基础形式

         C++的代码一般是用'.cpp'文件引入头文件'.h',具体格式如下:

b.头文件中的防卫式声明

   在头文件中加入防卫式声明,是为了防止头文件被二次引用。如何实现呢?请看语法:

(二)头文件主体内容介绍一(以Complex类为例,没有指针的class)

a.数据

  1. 在C++的Class中,为了保护数据的安全性,会把数据写成private(私有的)。那么外界如何获取数据?可以通过public的读数函数获取。

 2. 以Complex 类为例,复数有是 实部 和 虚部的,那么在这个类中就应该有两个数据来表示 实部 和 虚部的,将其命名为re 和 im。代码如下:

class complex{
 private :
   double re,im;
};

b.函数:

 1. 函数和数据不一样,在类中函数的可以是public,可以是private,也可以是protected,看你设计的需求。

 2. 在类中的函数大致要有这几类:构造函数、析构函数、成员函数、全局函数。

构造函数:

       构造函数在名字上和类名一样,它是用来在其他人创建这个类的对象的时候进行初始化的,所以不需要类型,同时它不需要用户调用,而是在创建对象时自动执行;

       构造函数通常是public,当然也用使用private的情况,当使用单例设计模式时,会将构造函数私有化结合Static关键字,详细的之后再谈,或者自己百度。构造函数的一种写法如下:

class complex{
 public :
   complex(double r = 0;double i =0 ) //在参数之后用‘=’为参数设置默认实参
     :re(r) ,im(i) //使用初始列(initialization list)方法为数据赋值,将参数值传给数据,构造函数特有,其他成员函数不可。
     {}//和其他成员函数一样写操作,但构造函数的操作一般时赋值,上行已赋值
    
 private :
   double re,im;
};

       其实构造函数有多种写法,这就不一一列举了。因为构造函数也可以重载(overloading)。

       重载是指在一个类中定义多个方法名相同但参数个数、类型或顺序不同的方法,从而实现对参数不同的方法进行区分。那你又要问了,啊不是说程序不是不能有二义性吗?那你同名了编译器不就不知道你在创建对象要用哪一个构造函数来创建吗?NoNo,兄弟姐妹们,咋别把计算机和编译器想得那么傻,它把我们的源代码编译成目标文件,在这份文件中不同的重载函数会被编译成不同名字的构造函数(目标文件函数的命名时根据:源代码的名称、参数等来的),编译器是通过识别这份目标文件中的名字来判断需要使用哪一个函数。想了解更多可查看编译原理相关书籍或文章。

析构函数

      不带指针的Class一般不用写,用来释放动态分配的内存空间的。在文章后面String类中再介绍。

成员函数

       在Complex中,为复数设置获取实部和虚部的公共函数,以便外部可获取私有化的数据,代码如下:

double real (){
   return re;
 }
double imag (){
  return im;
}

       const常量成员函数

        我们也可以看出上述函数只是将数据返回,并没有对数据进行修改,一直保持常量所以我们可以为其加上const修饰,防止在函数中数据被误改。修改之后代码如下:

class complex{
 public :
   complex(double r = 0;double i =0 ) //在参数之后用‘=’为参数设置默认实参
     :re(r) ,im(i) //使用初始列(initialization list)方法为数据赋值,将参数值传给数据,构造函数特有,其他成员函数不可。
     {}//和其他成员函数一样写操作,但构造函数的操作一般时赋值,上行已赋值
     
  double real () const { return re; }
  double imag () const { return im; }
  
  
 private :
   double re,im;
};

        为什么要加上const修饰?

        当一个成员函数被声明为 const 时,它承诺不会对调用它的对象进行任何修改。 为了防止其他人在使用const对象调用编译器报错。解释例子如下:

         inline函数

        一般在类中声明定义的就可能是内联函数。为什么说可能呢?其实判断一个函数是否是内联函数是由编译器决定,在类中声明定义 简单的函数 在编译的时候就会将其判断成内联函数,过于复杂的成员函数,编译器也不会将其视为内联函数。

         像下面简单的real()和imag()应该就是内联函数了。

  double real () const { return re; }
  double imag () const { return im; }

        内联函数在执行速度会比普通函数要快,这也是它的优点之一。那它那么快,全局函数可以是内联函数吗?当然可以啦!咱们编译器也是一个很听劝的人,我们可以加上inline关键字告诉编译器,我们建议你将它也编译成内联函数。如下:

inline //在函数前加上一个关键字
double liru (const complex& x)
{
  return x;
}

         参数的传递  和 返回值传递       

        参数的传递分为:值传参、引用传参、常量引用传参。

值传参:传递的参数是普通的变量,接受到之后将值压入栈中。如前面的列举的构造函数传参方式。

引用传参:传递的是指针。显然参数是数组等较大内存的参数时,使用4字节的指针传递速度更快。用'&'在类型后面跟表示引用传参,如下:

complex& operator += (complex& a);
//此为操作符'+='的重载,后面文章也会介绍到,此时只要知道引用传参的用法即可。

 常量引用传参:当我们使用引用传参时,在函数中对数据的操作会 影响实参,当我们不需要改变实参的数据时,在传递参数时可以加上const来表示不需要改变实参的值

complex& operator += (const complex&);

        返回值的传递基本同上,分为值返回和引用返回,但值得注意的是在返回值传递上要考虑是否可以使用引用返回。比如,如果返回值是在类中定义的非静态数据,此时的传递方式为引用返回,类的生命周期在class结束调用时就结束了。此时返回的指针地址将会消失,返回错误的数据。

        friend 友元函数

        前面提到说数据是private的,不能被外界访问。如果某个对象或函数一定要直接通过拿到数据,就可以将这个函数设置为友元函数。语法如下:

friend complex& __doapl (complex *, const complex&);//在complex类中将函数_doap1设置为朋友

inline complex&
__doapl (complex* ths, const complex& r)
{
  ths->re += r.re;//在函数实现就可以直接对私有的数据进行访问
  ths->im += r.im;
  return *ths;
}

        虽然友元函数可以直接取数据,操作比函数快,但是使用友元函数就打破了类的封装性,所以尽量不要设置太多友元函数。毕竟在人生中,也不是越多朋友越好。

        此外,同一个class创建出来的对象互为友元函数,可以相互直取对方的私有数据。

        操作符重载

        学过复数都知道,复数也是可以进行计算的。复数的加法就是实部+实部,虚部+虚部,所以我们为复数设计一个加法时,就可以对操作符 '+=' 进行重载。在介绍参数传递时,我已经把在complex类中的对'+='操作符定义,其他具体操作:

inline 
complex& __doapl (complex* ths, const complex& r)//实现操作
{
  ths->re += r.re;
  ths->im += r.im;
  return *ths;
}
 
inline 
complex& complex::operator += (const complex& r)//操作符重载函数
{
  return __doapl (this, r);
}

        一起分析操作。

class A{
    complex c1(2.5,3.3);
    complex c2(2.5,3.3);
    c1 += c2;//调用 +=操作符
}

        不知道你是否有产生疑问,在+=函数中,参数传递只有一个,那么函数怎么知道是那两个对象操作?

        其实在C++中,编译器会为成员函数设置一个this指针,谁调用函数了,谁就是this指针(this指针同时也是确定函数要不要设计为成员函数的参考),在例子中显然是c1调用了函数,所以this是指向c1的指针。

Tip1:在编写时,不能在参数传递写上 this,但是可以在函数体内直接使用。

Tip2:自己可以分析这两段回顾一下const关键字、inline函数、参数传递与返回。

全局函数

        全局函数的参数里面没有this指针,在语法上和成员函数的区别如下:

(三)头文件主体内容介绍二(以String类为例,有指针的class)

a.Big three

        在前面的介绍中,有提到析构函数一般是只有指针的类才需要用来释放动态分配的空间。其实关于 需要拷贝 且 是指针型的类我们还都需要写 拷贝构造函数、拷贝赋值以及析构函数,它们三者统称为 BigThree。

析构函数

        它的主要作用就是当生成的对象死亡时,调用它释放空间,语法如下:

inline
String::~String()
{
   delete[] m_data;
}

拷贝赋值函数

        其实有一个默认的拷贝赋值函数,但是对于指针来说只是浅拷贝(只是换了指针的指向,并不是将内容更换成对应的数据内容),同时也会造成内存泄漏的问题,以a拷贝到b中解释一下。 

        语法如下:

        从拷贝图也大致了解拷贝赋值的三大流程(以a拷贝到b中:b = a):

1.先将b的内存杀死清空;

2.给b创建和a一样大小的空间;

3.将指针a指向的内容拷贝到b新建的空间内存里。

Tip:同时要注意如果将同一个指针进行拷贝时,结果就会错误(我们的第一步是先杀死b的内存,如果两者是同一指针,那么在第三步就无法拷贝),所以我们要先判断拷贝是否为同一数据。具体实现如下:

inline
String& String::operator=(const String& str)
{
   if (this == &str)//判断
      return *this;

   delete[] m_data;//1
   m_data = new char[ strlen(str.m_data) + 1 ];//2,  +1是为了给结束符号
   strcpy(m_data, str.m_data);//3
   return *this;
}

拷贝构造函数

        其实有一个默认的拷贝构造函数,不用原因同上。

        语法如下:

        具体实现如下:

inline
String::String(const char* cstr)//要引入cstring头文件
{
   if (cstr) {//判断是否为空串
      m_data = new char[strlen(cstr)+1];
      strcpy(m_data, cstr);
   }
   else {   //就算是空,也要给人家放结束符号把
      m_data = new char[1];
      *m_data = '\0';
   }
}

b.堆、栈和内存管理

栈(stack)

        压入栈中的数据,都是有作用域的局部数据,当函数或类结束时,就会自动调用析构函数释放内存(不用程序员写析构函数)。

堆(heap)

        在堆中存储的数据,是动态分配(可以通过new和malloc)的,需要手动释放,不然会产生内存泄漏。

c.扩展补充

静态static

        静态函数是指在声明前面加上static关键字,限定外部变量或函数的作用域为本文件,可以避免内存消失和冲突,提高程序性能。

        在前面提到成员函数存在this指针 以及 数据在作用域之外就会死亡,但是static不同。

        首先是静态数据,它只有在程序结束时才会死亡,这与它存放在全局变量区有关(这涉及到内存四大区,感兴趣可以另外学习)。此外,静态函数定义之后,还需要在class外面声明(赋初始值)。例子如下:

class A
{
public:
  static int* m_data;
};
int A::m_data = 3;//赋初值

        关于静态函数有两个注意的点:编译器不会给它补充this指针 以及 静态函数只能处理静态数据。

        静态函数的使用方法(两种):

模板template

        模板是泛型编程的基础,所谓模板,实际上是建立一个通用函数或类,其类内部的类型和函数的形参类型不具体指定,用一个虚拟的类型来代表。

       在这里分为类模板和函数模板介绍。

       类模板(class template)

        使用场景:当有两个或多个类,其功能是相同的,仅仅是数据类型不同。

        使用方法:使用关键字 template和typename 定义:

template <typename T>

        具体例子及解析:

        函数模板

        使用场景:类内部的类型和函数的形参类型不具体指定。

        使用方法:

template <class T>

        具体例子及解析:

命名空间namespace

        命名空间是为了防止在协同开发时,不同开发者取到名字重复,故将自己开发的程序封装成命名空间。

        使用方法(最常见的std命名空间为例):

1. using namespace std;//将std空间全部打开

2. using std::cout;//一条条打开

3.std::cout<<'hello,world';//使用时在打开

四、面向对象

(一)类与类之间的三大基本关系

a.复合关系(compostion)

基础介绍

        C++中的复合关系可以理解为has - a 的关系,意思就是说两个类A、B,类A中有类B的对象。关系图及类图如下: 

        在复合关系下,类A可以使用B的成员函数,举个代码例子:

class A{
    protected:
    B b;//类A中定义一个B的对象,这就是复合关系
    //此外A和B对象同生同死
    public:
    int abc(){ return b.abc();}//类A可以直接使用B的函数
    double aqw(){return b.aqw();}
}

        在代码中可以清楚的看见类A 的成员函数都可以直接通过B的函数来实现,这种特殊的例子就是适配器(Adapter)设计模式(23种设计模式之后我也复习到和大家分享~),其中我们的对象b就是一个适配器,为了满足客户提出的新需求在之前已经完全实现了。

复合关系下的构造函数和析构函数

        我们已知构造是创建,析构是消亡,那么对于一个类拥有另一个类的关系两者之间的顺序是什么呢?

        答案揭晓:

1.构造函数由内到外创建,意思是先创建B的构造函数再使用A的。(今天早上看侯老师的另一个课程,有一句"勿在浮沙筑高台",也可以理解这个,肯定是要内强创建了,再向外发展。)

2.析构函数由外向内。(像剥洋葱,如果你愿意一层一层拨开我的心~~~)

        Tip:你要可以在构造和析构函数中写一个输出语句,看结果的输出顺序来验证是否就有。

b.委托关系(delegation)

        委托关系其实也是一个has-a的关系,它和复合的区别在于: "a"是一个指针,这就表明它其实和A来说并不在一个生命周期中,非同生共死,只有当A需要调用它的时候才会出现。类图就是将复合关系菱形变成空心的。

        在应用上,其实A可以作为一个接口与客户连接,在修改功能是也不会随便动A,只会在B中修改代码,所以修改功能时也不会影响客户端,这种做法就是编译防火墙

c.继承关系(inheritance)

        继承关系表示的是:is -a ,当A继承了B时,A就可以使用B中的函数和数据。类图如下:

        子类对于父类是完全继承的,包括构造函数和析构函数。在写父类的析构函数时要将其写成virtual函数(虚函数),不然在编译的过程中会产生未定义行为报错(undefined behavior)。

虚函数virtual

        当父类是虚函数,表明子类可以复写(重新定义函数里面的操作)这个函数。它的分类如下:

1.虚函数(virtual):子类可以复写这个函数。(不强制,因为在父类中有默认操作。)

2.纯虚函数(pure virtual):子类必须复写这个函数。(强制,父类中没有默认操作)

        两者语法如下:

class A{
    public:
    virtual int abc(){ return 'hello';}//虚函数
    virtual double aqw()=0;//纯虚函数
}

(二)关系的组合

        在这一部分,两者的关系组合涉及到较多的设计模式,这部分我不太记得了所以详细的介绍等复习完再写一篇,这就简单介绍一种组合让大家知道可以合在一起使用。

        以委托和继承关系组合为例,它们就可以组成“观察者设计模式”,类图构造如下:

        主体(subject)作为数据方,观察者(observe)作为更新数据的接受方。在subject定义了n个(n>=1)observe指针对象,当subject中的数据发生变化会通过函数告诉observe,其他类可以通过继承observe中的更新函数(虚函数)复写操作,就这样不用一个个修改更新的数据。

五、小小的总结

        这个文章就只是简单的介绍一些C++的基本语法和基本知识,对于一些要深入理解的知识,本人能力有限,可能只是提供一些思路和思考,最后也感谢你可以耐心的看完这篇长文。如果我的产出可以帮助到你我也会很高兴。

        在学习了C++ 面向对象基础之后发现自己学习的知识真的是太少了,还是用那句话来勉励自己和送给大家————革命尚未成功,同志仍需努力。

  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值