Tian Jiale's Blog

C++ 面向对象

引用

引用:

int n = 4;
int & r = n; // r引用了n,r的类型是int &

某个变量的引用,等价与这个变量,相当于这个变量的别名

常引用:

int n=4;
const int & r = n; // r的类型是const int &
r = 200; // 编译错误
n = 300; // 编译正确

不能通过常引用去修改引用的内容

常引用和非常引用的转换:

const T &和 T &是不同的类型!!!

T &类型的引用或 T &类型的变量可以用来初始化 const T &类型的引用。

const T &类型的常变量和 const T &类型的引用则不能用来初始化 T &类型的引用,除非进行强制类型转换。

const 关键字

定义常量:

const int ML = 100;
const string NAME = "Tom";

定义常量指针:

// 不可通过常量指针修改其指向的内容
int n, m;
const int *p = &n;
*p = 5; // 编译错误
n = 4; // OK
p = &m; // ok,常量指针的指向可以变化

常量指针不能赋值给非常量指针,反过来可以

通过强制类型转换(int *)可以将常量指针赋值给非常量指针

应用:

函数参数为常量指针,可避免内部不小心改变参数指针所指地方的内容

定义常引用:

不能通过常引用去修改引用的变量

int n;
const int & r = n;
r = 5;// 错误
n = 4;// 正确

动态内存分配

使用 new 运算符分配存储空间

1.分配一个变量

P = new T;

T 是任意类型名,P 是类型为 T*的指针

动态分配出一片大小为 sizeof(T)字节的内存空间,并且将该内存空间的起始地址赋值给 P。例:

int *pn;
pn = new int;
*pn = 5;

2.分配一个数组

P = new T[N];

T:任意类型名

P:类型为 T*的指针

N:要分配的数组元素的个数,可以是整形表达式

动态分配出一片大小为 sizeof(T)*N 字节的内存空间,并且将该内存空间的起始地址赋值给 P。

使用 delete 释放分配出的空间

1.释放一个变量

int *p = new int;
*p = 5;
delete p;
delete p; // 异常,一片空间不能被delete多次

2.释放一个数组

int *p = now int[10];
p[0] = 1;
delete []p; // 该指针必须指向分配出来的数组,[]必须加,否则只释放一个变量的空间,而其余的将无法释放进而形成内存碎片

内联函数

1.函数调用是有时间开销的。如果函数本身只有几条语句,执行非常块,而且函数被反复执行很多次,相比 之下调用函教所产生的这个开销就会显得比较大。

2.为了减少函数调用的开销,引入了内联函数机制。编 译器处理对内朕函数的调用语句时,是将整个函数的 代玛插入到调用语句处,而不会产生调用函数的语句。

例:

inline MAX(int a, int b){
   if(a > b) return a;
   return b;
}

函数重载

一个或多个函数,名字相同,然而参数个数或参数类型不同,这叫作函数的重载。

int MAX(int n1, int n2){  }
int MAX(double f1, double f2){  }
int MAX(int n1, int n2, int n3){  }

函数重载使函数命名简单

编译器通过调用语句中的函数实参的类型和个数判断应该调用哪个函数

使用时应避免函数的二义性

函数的缺省参数

C++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候.若相应位置不写参 数,参数就是缺省值。

void func(int a, int b = 1, int c = 1){  }

func(10); // 等效于func(10,1,1)
func(10,4); // 等效于func(10,4,1)
func(10,4,7);
func(10, ,7); // 不行,只能最右边的连续若干个参数缺省

函数参数可缺省的目的在于提高程序的可扩充性

在函数拓展时,只需增设缺省函数,原程序就不必修改

类概述

  1. 概念

    类是定义同一类所有对象的变量和方法的蓝图或原型

    对象是由类定义的

    一个对象有属性和方法,属性是变量,方法是函数

  2. 实例

    class student{
      public:
        int age;
        string name;
        int sex;
        void init(int age, string name, int sex_){
          age = age, name = name, sex = sex_;
        }
        string namechange(string name_){
          name = name_;
        }
    }; // 必须有分号
    
    int main(){
      student A;// A是一个对象
      return 0;
    }
    
  3. 对象的内存分配

    对象所占的内存空间大小,等于所有成员变量的大小之和

    每个对象都有自己的内存空间,一个对象的成员变量改变不会影响其他对象

  4. 对象间的运算

    可以使用“=”进行赋值运算,但不能使用“==”“!=”等运算符进行比较,除非进行运算符重载

  5. 使用类的成员变量和成员函数

    用法一:对象.成员名

    student A;
    A.init(10, "Tom,1");
    A.name = "Jack";
    

    用法二:指针->成员名

    student A;
    shudent *p = &A;
    p->init(10, "Tom,1"); // init()作用在p指向的对象上
    p->name = "Tom";
    

    用法三:引用.成员名

类成员可访问范围

在类的定义中,用下列访问范围关键词来说明类成员可被访问的范围:

private:私有成员,只能在成员函数内访问

public:公有成员,可以在任意地方访问

protected:保护成员

以上三种关键字出现的次数和先后次序都没有限制。

class className{
  private:
     私有成员和属性
  public
     公有属性和函数
  protected
     保护属性和函数
};

如果某个成员前面没有上述关键词,则缺省的被认为是私有成员

在类的成员函数内部,能够访问:

当前对象的全部属性、函数,同类其他对象的全部属性、函数。

在类的成员函数以外的地方,只能够访问该类对象的共有成员

设置私有成员的机制,叫“隐藏”

“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改。

与 struct 的区别是 struct 的未声明变量是公有成员

类成员函数的重载和函数缺省

成员函数也可以重载

成员函数也可以带缺省函数(注意避免二义性)

构造函数

  1. 基本概念

    成员函数的一种

    名字与类名相同,可以有参数,不能有返回值(void 也不行)

    作用是对对象进行初始化,如给成员变量赋初值

    如果定义类是没写构造函数,则编译器生成一个默认的无参数的构造函数(默认构造函数无参数,不做任何操作)

    如果定义了构造函数,则编译器不生成默认的无参数的构造函数

    对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数

    一个类可以有多个构造函数

  2. 例:

    class student{
      public:
        int age;
        string name;
        student(int age = 10, string name = "Tom"){
          age = age, name = name;
        }
    };
    

    构造函数最好是 public 的,private 构造函数不能直接用来初始化对象

  3. 构造函数在数组中应用

    初始化过程也会调用构造函数

复制构造函数

  1. 基本概念

    只有一个参数,及对同类对象的引用

    形如 X::X(X&)或 X::X(const X &),二者选一

    后者以常量对象作为参数

    不允许有形如 X::X(X)的构造函数

    如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能

    例:

    class Complex{
      private:
        double real,imag;
      public:
        Complex(const Complex & c){
            real=c.real,imag=c.imag;
        }
    };
    
  2. 起作用的三种情况

    1. 当用一个对象去初始化同类的一个对象时

      Complex c2(c1);
      Complex c2 = c1; // 初始化语句,非赋值语句
      
    2. 如果某函数有一个参数是类 A 的对象。那么该函数调用时,类 A 的复制构造函数将被调用

    3. 如果某函数的返回值是类 A 的对象,则函数返回时,A 的复制构造函数被调用时起作用。

      对象间赋值并不导致复制构造函数被调用

类型转换构造函数

定义转换构造函数的目的是实现类的自动转换

只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数

当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)

例:

class Complex{
  private:
     double real, imag;
  public:
     Complex(int c){
        real = c, imag = 0;
     }
};
// 显式类型转换构造函数:
class Complex{
  private:
     double real, imag;
  public:
     explicit Complex(int c){
        real = c, imag = 0;
     }
};
Complex c1;
c1 = 9:// error 9不能被自动转换成一个临时Complex对象
c1 = Complex(p);// OK

析构函数

名字与类名相同,在前面加“~”,没有参数和返回值,一个类最多只能有一个析构函数

析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。

如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做

如果定义了析构函数,则编译器不生成缺省的析构函数

例:

class String{
  priviate:
     char *p;
  public:
     String(){
       p=new char[10];
     }
    ~String(){
       delete []p;
     }
};

析构函数和数组:

对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用

this 指针

作用:

其作用就是指向成员函数所作用的对象

非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针

例:

this->real++;// 等价于 real++

在函数中可使用*this 返回当前对象

对象的空指针可以调用不使用成员变量的函数

静态成员函数中不能使用 this 指针!

因为静态成员函数并不具体作用与某个对象!

因此,静态成员函数的真实的参数的个数,就是程序中写出的参数个数。

静态成员

  1. 基本概念

    在定义前面加了 static 关键字的成员

    注意:

    1.普通成员变量每个对象有各自的一份,而静态成员变量一共就有一份,为所有对象共享。

    2.普通成员函数必须作用于某个对象,而静态成员函数并不具体作用于某个对象

    3.静态成员不需要通过对象就能访问

    4.静态成员变量本质上是集成到类中的全局变量,即使此类不存在一个对象,该静态成员变量也存在

    5.必须在定义类的文件中对静态成员变量进行一次说明或初始化。否则编译能通过,链接不能通过

    6.在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数

    7.sizeof 运算符不会计算静态成员变量

    例:

    class CRectangle{
      private:
        int w, h;
        static int nTotalArea; // 静态成员变量
        static int NtotalNumber; // 静态成员变量
      public:
        CRectangle(int w, int h);
        ~CRectangle();
        static void PrintTotal(); // 静态成员函数
    };
    
  2. 访问方法

    1.类名::成员名

    CRectangle::PrintTotal();
    

    2.对象名.成员名

    CRectangle r;
    r.PrintTotal();
    

    3.指针->成员名

    CRectangle *p = &r;
    p->PrintTotal();
    

    4.引用.成员名

    CRectangle &ref=r;
    int n =ref.nTotalNumber;
    
  3. 应用案例

    构造函数中对矩形数量单增,矩形面积增加

    特别注意在复制构造函数中要有构造函数相同的对静态成员变量的修改

    析构函数中对矩形数量单减,矩形面积减小

初始化链表

class CTyre
{
  private:
     int radius;
     int width;
  public:
     CTyre(int r, int w):radius(r),width(w){ }
};

radius(r)相当于 int radius(r);是初始化。

成员对象初始化列表中的参数可以是任意对象的表达式,可以包括函数、变量,只要表达式中的函数或变量有定义就行。

成员对象和封闭类

  1. 基本概念

    成员对象是指一个类的成员是一个类的对象的此对象

    封闭类是指含有成员对象的类

  2. 实例

    class CTyre
    {
      private:
        int radius;
        int width;
      public:
        CTyre(int r,int w):radius(r),width(w){ }
    };
    
    class CEngine
    {
    };
    
    class CCar{
      private:
        int price;
        CTyre tyre;
        CEngine engine;
      public:
        CCar(int p,int tr,int tw):price(p),tyre(tr,w){ }
        // tyre使用构造函数初始化,engine使用自动生成的无参构造函数初始化
    };
    

    上例中,如果 CCar 类不定义构造函数,则下面的语句会编译出错:

    CCar car;

    因为编译器不明白 car.tyre 该如何初始化。 car.engine 的初始化没问题,用自动生成的无参构造函数即可。

    任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。

  3. 封闭类构造函数和析构函数的执行顺序

    (1)封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类的构造函数。

    (2)对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与他们在成员初始化列表中出现的次序无关。

    (3)当对象类的对象消亡时,限制性封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。

常量对象、常量成员函数

  1. 基本概念

    常量对象:如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加 const 关键字。

    常量成员函数:在类的成员函数说明后面加上 const 关键字,则该成员函数称为常量成员函数。

    常量成员函数执行期间不应修改其所作用的对象。因此,在常量成员函数中不能修改成员变量的值(静态成员变量除外),也不能调用同类的非常量成员函数(静态成员函数除外)。

  2. 实例

    class Sample
    {
      public int value;
      void GetValue() const
      {
        value = 0; // wrong
        func(); // wrong
      }
      void func();
      Sample(){}
    };
    
  3. 常量成员函数的重载

    两个成员函数,名字和参数表都一样,但一个是 const,一个不是,算重载。

    调用时 常量成员对象调用常量成员函数,非常量成员对象先找非常量成员函数,如果没有调用常量成员函数。

友元

友元分为友元函数和友元类

(1)友元函数:一个类的友元函数,可以访问该类的私有成员

​ 可以将一个类的成员函数(包括构造、析构函数)说明为另一个类的友元。

(2)友元类:如果 A 是 B 的友元类,那么 A 的成员函数可以访问 B 的私有成员

友元使一个类可以修改其成员对象的私有成员,如果未声明友元则不可访问或修改其成员对象的私有成员。

友元类之间的关系不能传递,不能继承

运算符重载

基本概念

  1. 运算符重载的需求

    C++预定义的运算符,只能用于基本数据类型的运算:整型、实型、字符型、逻辑型……

    +、-、*、/、%、^、&、»、!=、=、«、|、!……

    目的:实现预期功能、使代码更简洁

  2. 基本概念

    运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同的数据时导致不同类型的行为。

    目的:拓展 C++中提供的运算符的适用范围,使之能作用于对象。

  3. 运算符重载的形式

    (1)运算符重载的实质是函数重载

    (2)可以重载为普通函数,也可以重载为成员函数

    (3)把含运算符的表达式转换成对运算符函数的调用

    (4)把运算符的操作数转换成运算符函数的参数

    (5)运算符被多次重载时,根据实参的类型决定调用哪个运算符函数

  4. 运算符重载的形式

    返回值类型 operator 运算符(形参表)
    {
    }
    

    重载为成员函数时,参数个数为运算符目数减一

    重载为普通函数时,参数个数为运算符目数

赋值运算符的重载

  1. 赋值运算符‘=’重载

    “=”只能重载为成员函数

    class String{
        private:
            char *str;
        public:
            String():str(new char[1]) { str[0] = 0; }
            const char * c_str() { return str; };
            String & oprator = (const char *s);
            String::~String() { delete[] str; }
    };
    String & String::operator = (const char *s)
    {   // 重载“=”以使得obj=“hello”能够成立
        delete[] str;
        str = new char[strlen(s)+1];
        strcpy(str, s);
        return *this;
    }
    
  2. 深拷贝和浅拷贝(复制构造函数应同深拷贝)

    浅拷贝:

    原生的“=”只能使两个对象的内存完全相同

    而如果成员有指针,只改变了指针指向的地址,而产生一系列问题

    深拷贝:

    重载“=”重新分配内存空间,深度拷贝

    深拷贝的问题:

    s = s;
    

    此时直接 delete[] str; 会导致逻辑错误,因此重载时先判断指针指向的地址是否相同,再行处理

    String & String::operator = (const char *s)
    {   // 重载“=”以使得obj=“hello”能够成立
        if(this = & s)
            return *this;
        delete[] str;
        str = new char[strlen(s)+1];
        strcpy(str, s);
        return *this;
    }
    
  3. 对 operator=返回值类型的讨论

    对运算符重载时,应尽量保留原运算符的特性

    a = b = c;
    (a = b) = c; // 会修改a的值
    

运算符重载为友元

重载为成员函数不够用了

(1,1)+1;

1+(1,1);

复数与实数相加

流插入运算符和流提取运算符的重载

  1. 流输入运算符的重载

    cout 是在 iostream 中定义的,ostream 类的对象

    “«”能用在 cout 上是因为,在 iostream 中对“«”进行了重载。

    ostream & ostream::operator<<(int n)
    {
      ……// 输出n的代码
      return *this;
    }
    

    实际应用时使用的是普通函数的重载

    class Student
    {
      public:
        int age;
    };
    Student a;
    ostream & ostream::operator<<(ostream & o,const Student & as)
    {
      o<<as.age;
      return o;
    }
    cout<<a;
    
  2. 流输出运算符的重载

    istream & operator>>(istream & is,Complex & c)
    {
        string s;
        is>>s;
        int pos = s.find("+",0);
        string sTmp = s.substr(0,pos);
        // 分离出代表实部的字符串
        c.real = atof(sTmo.c_str());
        // atof库函数能将const char*指针指向的内容转换成float
        sTmp = s.substr(pos+1,s.length()-pos-2);
        // 分离出代表虚部的字符串
        c.imag = stof(sTmp.c_str());
        return is;
    }
    

类型转换运算符的重载

#include <iostream>
using namespace std;
class Complex
{
    double real,imag;
    public:
        Complex(double r = 0,double i = 0):real(r),imag(i){ };
        operator double () { return real; }
        // 重载强制类型转换运算符 double
};
int main()
{
    Complex c(1.2,3.4);
    cout << (double)c << endl;// 输出1.2
    double n = 2 + c;// 等价于 double n=2+c.operator double()
    cout<<n;// 输出 3.2
    return 0;
}

自增自减运算符的重载

++、–有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,c++规定

  • 前置运算符作为一元运算符重载

    重载为成员函数:

T & operator++();
T & operator--();

重载为全局函数:

T1 & operator++(T2);
T1 & operator--(T2);
  • 后置运算符作为二元运算符重载,多写一个没用的参数

    重载为成员函数:

T & operator++(int);
T & operator--(int);

重载为全局函数:

T1 & operator++(T2,int);
T1 & operator--(T2,int);

但是在没有后置运算符重载而有前置重载的情况下,

在 vs 中,obj++ 也调用前置重载,而 dev 则令 obj++ 编译出错

前置形式的++、–返回变量的引用

后置形式的++、–返回临时变量

–i 更快

运算符重载的注意事项

1.C++不允许定义新的运算符

2.重载后运算符的含义应该符合日常习惯

3.运算符重载不改变运算符的优先级

4.一下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof;

5.重载运算符()、[]、->或赋值运算符=时,运算符重载函数必须声明为类的成员函数。

继承和派生的基本概念

继承:在定义一个新的类 B 时,如果该类与某个已有的类 A 相似(指的是 B 拥有 A 的全部特点),那么可以把 A 作为一个基类,而把 B 作为基类的一个派生类(也称子类)。

派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数。

派生类一经定义后,可以独立使用,不依赖于基类。

派生类拥有基类的全部成员函数和成员变量,不论是 private、protected、public。

在派生类的各个成员函数中,不能访问基类中的 private 成员。

派生类的写法:

class 派生类名:public 基类名
{

};

派生类中的函数可以对基类函数进行覆盖,但依旧可以调用基类的相同函数。

派生类对象的内存空间

派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。**在派生类对象中,包含着基类对象,**而且基类对象的存储位置位于派生类对象新增的成员变量之前。

继承关系和复合关系

类之间的两种关系

1,继承:“是”关系

继承 A,B 是基类 A 的派生类。

逻辑上要求:“一个 B 对象也一个 A 对象”。

2.复合:“有”关系

类 C 中“有”成员变量 k,k 是类 D 的对象,则 C 和 D 是复合关系

一般逻辑上要求:“D 对象是 C 对象的固有属性或组成部分”。

对于人养狗的情况,使用指针分别指向狗和人

派生类覆盖基类成员

派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要用作用域符号::

成员类型访问权限:

类的 private 成员:

​ - 基类的成员函数

​ - 基类的友元函数

基类的 public 成员:

​ - 基类的成员函数

​ - 基类的友元函数

​ - 派生类的成员函数

​ - 派生类的友元函数

​ - 其他函数

基类的 protected 成员:

​ - 基类的成员函数

​ - 基类的友元函数

​ - 派生类的成员函数可以访问当前对象的基类的保护成员

派生类的构造函数

class Bug
{
    private:
        int nLegs;
        int nColor;
    public:
        int nType;
        Bug(int legs, int color);
        void PrintBug(){ };
}

class FlyBug: public Bug
{
        int nWings;
    public:
        FlyBug(int legs, int color, int wings);
}

Bug::Bug(int legs, int color)
{
    nLegs = legs;
    nColor = color;
}

// 错误的FlyBug构造函数
FlyBug::FlyBug(int legs, int color, int wings)
{
    nlegs = legs;// 不能访问
    nColor = color;// 不能访问
    nType = 1;// OK
    nWings = wings;
}

// 正确的FlyBug构造函数
FlyBug::FlyBug(int legs, int color, int wings):Bug(legs,color)
{
    nWing = wings;
}

在创建派生类的对象时,需要调用基类的构造函数初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。

调用基类的构造函数的两种方法

- 显式方式:在派生类的构造函数中,为基类的构造函数提供参数。

​ derived::derived(arg_derived-list):base(arg_base-list)

- 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数。

派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。

封闭派生类对象的构造函数的执行顺序

基类的构造函数->成员对象类的构造函数->派生类自己的构造函数

公有继承的赋值兼容规则

public 继承的赋值兼容规则:

1.派生类的对象可以赋值给基类对象

2.派生类对象可以初始化基类引用

3.派生类对象的地址可以赋值给基类指针

直接基类和间接基类

A 派生出 B,B 派生出 C,C 派生出 D,D 派生出 E……

可以连续继承