C++学习笔记

摘要:

笔记内容来自中国大学慕课北京大学程序设计与算法(三)C++面向对象程序设计课程

引用

  • 引用是一个别名,引用变量和原变量相当于是同一个东西。

  • 定义引用时一定要将其初始化成引用某个变量,不能引用常量和表达式

  • 初始化后,他就一直引用该变量,不会再引用别的变量了。

  • double a = 4, b = 5;
    double & r1 = a;
    r1 = b;
    cout << a;
    <!--0-->
    

函数重载

  • 一个或者多个函数,名字相同,但是参数个数或者参数类型不同,这就叫函数的重载

函数的缺省参数

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

    1
    2
    3
    4
    void func (int x1, int x2 = 2, int x3 = 3){}

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

    缺省参数在进行函数功能扩充时很有用,这时候想在某个已经写好的函数最后增加新的参数时,就不用将调用该函数的位置全部修改了,此时只需要修改那些参数不是缺省值的位置。

构造函数

  • 自己写了构造函数后,编译器就不会再自动生成那个空的构造函数了。

拷贝构造函数

  • 只有一个参数,形如X::X(X&)或者X::X(const X &)。参数必须是引用
  • 一个类一定有并且只会有一个拷贝构造函数,要么是自己写的,要么是编译器生成的。
  • 如果某个函数有一个参数是类A的对象,那么该函数被调用时,类A的拷贝构造函数会被调用。如果函数返回值是类A的对象,则函数返回时,A的拷贝构造函数会被调用

类型转换构造函数

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

  • 只有一个参数,而且不是拷贝构造函数的构造函数,一般就可以看做是转换构造函数

  • 当需要的时候,编译系统会自动调用转换构造函数。

  • 一个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Complex   {
    public:
    double real, imag;
    Complex( int i) {//由于这个构造函数只有一个参数而且不是拷贝构造函数,所以他是类型转换构造函数
    cout << "IntConstructor called" << endl; real = i; imag = 0;
    }
    Complex(double r,double i) {real = r; imag = i; }
    };
    int main ()
    {
    Complex c1(7,8);
    Complex c2 = 12;//如果没有类型转换构造函数,这样做事不行的。有了类型转换构造函数后,由于12是int,可以作为类型转换构造函数的参数,12就会被转换为一个临时的Complex对象,然后赋值给c2
    c1 = 9; // 9被自动转换成一个临时Complex对象
    cout << c1.real << "," << c1.imag << endl; return 0;
    }

析构函数

  • 一个类只能有一个析构函数,析构函数没有类型和返回值

  • 如果在类中申请了空间,需要在析构函数中手动delete掉。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class String{ 
    private :
    char * p;
    public:
    String () {
    p = new char[10];
    }
    ~ String () ;
    };
    String ::~ String() {
    delete [] p;
    }
  • 若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)

this指针

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

静态成员变量

  • static

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

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

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

  • 访问静态成员的方式

    • 类名::成员名

      CRectangle::PrintTotal();

    • 对象名.成员名

      CRectangle r; r.PrintTotal();

    • 指针->成员名

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

    • 引用.成员名

      CRectangle & ref = r; int n = ref.nTotalNumber;

  • 使用静态成员时,就把它当做是全局变量

  • 设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解。使用全局变量可以达到和静态成员变量相同的效果,只不过全局变量不想静态成员一样被封装在类里面,所以维护起来不如静态成员方便

  • 类中的静态成员变量必须要在类定义后在类的外面显示声明,是否初始化均可。如果不做这一步,编译可以通过,但是链接会失败。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include <iostream>

    using namespace std;


    class CRectangle {
    private:
    int w, h;
    static int nTotalArea;
    static int nTotalNumber;
    public:
    CRectangle(int w_,int h_);
    ~CRectangle();
    static void PrintTotal();
    };

    CRectangle::CRectangle(int w_, int h_) {
    w = w_;
    h = h_;
    nTotalNumber++;
    nTotalArea += w * h;
    }

    CRectangle::~CRectangle() {
    nTotalNumber--;
    nTotalArea -= w * h;
    }

    void CRectangle::PrintTotal() {
    cout << nTotalNumber << "," << nTotalArea << endl;
    }

    int CRectangle::nTotalNumber = 0;
    int CRectangle::nTotalArea = 0;

    int main(){
    CRectangle r1(3, 3), r2(2, 2);
    CRectangle::PrintTotal();
    r1.PrintTotal();
    return 0;
    }

    第33、34行是必须的。并且此处不再需要加static关键字。

    上面的函数有一个缺陷:某中情况下,两个静态成员变量的值会不正确。这种情况是:在使用CRectangle类时,有时会调用复制构造函数生成临时的隐藏的CRectangle对象,即调用一个以CRectangle类对象作为参数的函数时, 调用一个以CRectangle类对象作为返回值的函数时。上面的函数中,我们没有定义自己的 拷贝构造函数,所以编译器会自己生成一个,在这个拷贝构造函数中,并没有对两个静态成员变量进行操作。但是,使用拷贝构造函数创建的对象在被销毁时,还是会调用我们写的析构函数,也就是将两个静态成员变量的值减一,这就导致了数值的不正确。临时对象在消亡时会调用析构函数,减少nTotalNumber 和 nTotalArea的值,可是这些临时对象在生成时却没有增加 nTotalNumber 和 nTotalArea的值。

    解决方式为,为这个类写一个拷贝构造函数,如下

    1
    2
    3
    4
    5
    CRectangle :: CRectangle(CRectangle & r ) {
    w = r.w; h = r.h;
    nTotalNumber ++;
    nTotalArea += w * h;
    }
  • 静态成员函数中,不能访问非静态成员变量,也不能访问非静态成员函数。原因很简单,静态成员函数并不作用于具体的某一个类上面,如果在其中访问了非静态的成员的话,没有办法知道访问的到底是哪个类的成员。

成员对象和封闭类

  • 有成员对象的类叫封闭类,所谓成员对象,即类中的成员是另一个类的对象。

  • class CTyre //轮胎类 
    {
            private:
                    int radius; //半径 
                  int width; //宽度
            public:
                    CTyre(int r,int w):radius(r),width(w) { }
    };
    
    class CEngine //引擎类 
    {
    };
    
    class CCar { //汽车类 private:
            private:
                  int price;
                  CTyre tyre;
                  CEngine engine;
        public:
            CCar(int p,int tr,int tw );
    };
    
    CCar::CCar(int p,int tr,int w):price(p),tyre(tr, w) {
    };
    
    int main()
    {
           CCar car(20000,17,225);
           return 0;
    }
    <!--6-->
  • 两个函数,名字和参数表都一样,但是一个是const,一个不是,算重载。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>
    using namespace std;
    class CTest {
    private :
    int n;
    public:
    CTest() { n = 1 ; }
    int GetValue() const { return n ; }
    int GetValue() { return 2 * n ; }
    };

    int main(){
    const CTest objTest1;
    CTest objTest2;
    cout << objTest1.GetValue() << "," <<
    objTest2.GetValue() ; return 0;
    }

友元

  • 友元分为友元函数和友元类两种

  • 友元函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class CCar ; //提前声明 CCar类,以便后面的CDriver类使用 class CDriver
    class CDriver
    {
    public:
    void ModifyCar( CCar * pCar) ; //改装汽车。由于这里使用了CCar的指针,所以才需要在前面提前声明一下。
    };
    class CCar
    {
    private:
    int price;
    friend int MostExpensiveCar( CCar cars[], int total); //声明友元
    friend void CDriver::ModifyCar(CCar * pCar); //声明友元 };


    void CDriver::ModifyCar( CCar * pCar) {
    pCar->price += 1000; //汽车改装后价值增加
    }

    int MostExpensiveCar( CCar cars[],int total) //求最贵汽车的价格
    {
    int tmpMax = -1;
    for( int i = 0;i < total; ++i )
    if( cars[i].price > tmpMax) tmpMax = cars[i].price;
    return tmpMax;
    }
    int main() {
    return 0;
    }
  • 友元类:如果A是B的友元类,那么A的成员函数可以访问B的私有成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class CCar {
    private:
    int price;
    friend class CDriver; //声明CDriver为友元类
    };
    class CDriver
    {
    public: CCar myCar;
    void ModifyCar() {//改装汽车
    myCar.price += 1000;//因CDriver是CCar的友元类,
    } };
    int main(){ return 0; }
  • 友元类之间的关系不能传递,不能继承。

运算符重载

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

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

  • 同一个运算符,对不同类型的操作数,所发生的行为不同。

  • 运算符重载的实质是函数重载

  • 遇到表达式中有重载运算符时,会把含运算符的表达式转换成对运算符函数的调用,然后把运算符的操作数转换成运算符函数的参数

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

  • class Complex {
        public:
            double real,imag;
            Complex( double r = 0.0, double i= 0.0 ):real(r),imag(i){}                     Complex operator-(const Complex & c);
    };
    Complex operator+( const Complex & a, const Complex & b) {
        return Complex( a.real+b.real,a.imag+b.imag); //返回一个临时对象 
    }
    Complex Complex::operator-(const Complex & c) {
        return Complex(real - c.real, imag - c.imag); //返回一个临时对象
    }
    <!--10-->
    
    - 赋值运算符重载时,返回值类型为引用,这样做的目的是尽量保留运算符原本的特性,理解不了就记住;与之对应的是,第21行要返回指针
    
    - 第1617行是很重要的,看这样一个例子
    
      <!--11-->
    
      如果没有1617行,那么此处第三行执行赋值操作时,会先delete掉str,之后又对str进行操作,会出错。
    
    - 第18行的delete操作是必须的,如果没有这个,构造函数中申请的内存会泄露。通俗讲,就是使用等于号进行赋值之前,要先将目前的释放掉,再去等于其他的。
    
    - 第19行需要开辟新的存储空间,即为深拷贝,不这样做的话就是浅拷贝,浅拷贝的危害是显而易见的。进一步思考,当我们想要执行一个赋值操作时,除了使用等于号可以达到目的,通过拷贝构造函数也可以达到赋值操作,所以就必须自己重新写拷贝构造函数,即9~12行,否则我们通过拷贝构造函数进行赋值时将会是浅拷贝。
    

运算符重载为友元

  • 一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    class Complex {
    double real,imag;
    public:
    Complex( double r, double i):real(r),imag(i){ };
    Complex operator+( double r );
    };
    Complex Complex::operator+( double r ) { //能解释 c+5
    }

    对上面这个例子,如果我们定义了一个Complex类的对象c,此时我们只能c + 5,不能5 + C。如果我们想解决这个问题,可以在类外面另外写一个普通函数

    1
    2
    3
    Complex operator+ (double r,const Complex & c) { //能解释 5+c
    return Complex( c.real + r, c.imag);
    }

    注意这个函数第一个参数是double,第二个参数是类对象。

    只是这样的话,新的这个重载函数不能访问类的私有成员,所以需要将这个函数重载为友元,即Complex类写成如下形式。

    1
    2
    3
    4
    5
    6
    7
    class Complex {
    double real,imag;
    public:
    Complex( double r, double i):real(r),imag(i){ };
    Complex operator+( double r );
    friend Complex operator + (double r,const Complex & c);
    };

继承

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

  • 派生类拥有基类的全部成员函数和成员变 量,不论是private、protected、public 。但是在派生类的各个成员函数中,不能访问 基类中的private成员。

  • 派生类的写法

    1
    2
    3
    class 派生类名:public 基类名 {

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

  • #include <iostream>
    #include <string> 
    using namespace std;
    class CStudent { 
      private:
            string name;
            string id; //学号
            char gender; //性别,'F'代表女,'M'代表男 int age;
        public:
            void PrintInfo();
            void SetInfo( const string & name_,const string & id_,
    int age_, char gender_ ); 
          string GetName() { return name; }
    };
    
    

class CUndergraduateStudent:public CStudent {//本科生类,继承了CStudent类
private:
string department; //学生所属的系的名称
public:
void QualifiedForBaoyan() { //给予保研资格
cout << “qualified for baoyan” << endl; }
void PrintInfo() {
CStudent::PrintInfo(); //调用基类的PrintInfo
cout << “Department:” << department <<endl; }
void SetInfo( const string & name_,const string & id_,
int age_,char gender_ ,const string & department_) {
CStudent::SetInfo(name_,id_,age_,gender_); //调用基类的SetInfo
department = department_;
} };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意24和28行调用基类函数的方式

- 继承关系与复合关系

创建继承关系时,不仅要考虑代码重合度是否高,还要考虑逻辑上是否正确。继承关系的逻辑是:如果A继承B,则A也是B。例如,如果man类继承human类,则man也是human。考虑这个情况:现在要定义一个点类Point和一个圆类Circle。点类中有坐标x和y,Circle类能否直接继承点类并加一个半径的成员变量呢?这时就要考虑下逻辑关系,一个圆是一个点吗?显然不是的,所以建立继承关系时不合适的,这时可以创建复合关系,如下

```cpp
class CPoint {
double x,y;
friend class CCircle; //便于Ccirle类操作其圆心
};

class CCircle {
double r;
CPoint center;
};

  • 考虑一个符合关系:每个人可以养10条狗,每只狗只有一个主人:

    错误示范:

    1
    2
    3
    4
    5
    6
    7
    class CDog; 
    class CMaster {
    CDog dogs[10];
    };
    class CDog {
    CMaster m;
    };

    上面的写法循环定义了,比如现在要求sizeof(CMaster),我们需要知道CDog的大小,而求CDog的大小又需要知道CMaster的大小。所以我们是求不出来的。改进方法如下。

    1
    2
    3
    4
    5
    6
    7
    class CDog; 
    class CMaster {
    CDog *dogs[10];
    };
    class CDog {
    CMaster *m;
    };
  • public继承的赋值兼容规则

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

      b = d;

      可以这样理解:如果基类对象b代表human,派生类对象d代表man,那么把man赋值给human没有问题,毕竟man也是human。下面的也可以这样理解

    • 派生类的对象可以初始化基类引用

      base & br = d;

      此时可理解为,基类引用指向派生类中的基类部分

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

      base * pb = & d;

覆盖

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

存取权限

  • 基类的private成员:可以被下列函数访问

    • 基类的成员函数
    • 基类的友元函数
  • 基类的public成员:任意位置

  • 基类的protected成员:

    • 基类的成员函数
    • 基类的友元函数
    • 派生类的成员函数可以访问当前对象的基类的保护成员。保护成员只能在派生类或者基类的成员函数或者通过友元被访问
  • 看一个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Father {
    private: int nPrivate; //私有成员
    public: int nPublic; //公有成员
    protected: int nProtected;
    };
    class Son :public Father{
    void AccessFather () {
    nPublic = 1; // ok;
    nPrivate = 1; // wrong
    nProtected = 1; // OK,访问从基类继承的protected成员
    Son f;
    f.nProtected = 1;//wrong ,f不是当前对象
    // 保护成员
    } };


    int main() {
    Father f;
    Son s;
    f.nPublic = 1; // Ok
    s.nPublic = 1; // Ok
    f.nProtected = 1; // error
    f.nPrivate = 1; // error
    s.nProtected = 1; //error
    s.nPrivate = 1; // error
    return 0;
    }

    第12行编译时会出错。需要注意,派生类的成员函数可以访问当前对象的基类的保护成员,当前类即AccessFather作用的那个对象,所以f并不是当前对象,所以f不能访问nProtected。

    接着来看22和24行,由于保护成员只能在派生类或者基类的成员函数或者通过友元被访问,所以这里main函数中不能访问保护成员。

虚函数和多态

  • 在类的定义中,前面有 virtual 关键字的成员函 数就是虚函数。

  • virtual 关键字只用在类定义里的函数声明中, 写函数体时不用

  • 构造函数和静态成员函数不能是虚函数

  • 多态有两种表现形式:

    • 通过基类指针调用基类和派生类中的同名虚函数时:

      (1)若该指针指向一个基类的对象,那么被调用是基类的虚函数;

      (2)若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

      这种机制就叫做“多态”

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class CBase { 
      public:
      virtual void SomeVirtualFunction() { }
      };
      class CDerived:public CBase {
      public :
      virtual void SomeVirtualFunction() { }
      };
      int main() {
      CDerived ODerived;
      CBase * p = & ODerived;
      p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
      return 0;
      }
    • 通过基类引用调用基类和派生类中的同名虚函数时:

      (1)若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;

      (2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。

      这种机制也叫做“多态”。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class CBase { 
      public:
      virtual void SomeVirtualFunction() { }
      };
      class CDerived:public CBase {
      public :
      virtual void SomeVirtualFunction() { }
      };
      int main() {
      CDerived ODerived;
      CBase & r = ODerived;
      r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
      return 0;
      }
  • 多态的作用

    在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

虚析构函数

  • 通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。

    解决办法:把基类的析构函数声明为virtual

    派生类的析构函数可以virtual不进行声明

    此时通过基类的指针删除派生类对象时,首先调用派生类的析构函 数,然后调用基类的析构函数

    一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。

    注意:不允许以虚函数作为构造函数

纯虚函数和抽象类

  • 纯虚函数:没有函数体的虚函数
    • 没有函数体指的是没有花括号,例如virtual void Print() = 0。
    • 对于virtual void Print(){},这个不是纯虚函数,他是有函数体的,只不过函数体为空
  • 如果一个类包含纯虚函数,那么这个类就是抽象类
    • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
    • 抽象类的指针和引用可以指向由抽象类派生出来的类的对象
  • 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部 不能调用纯虚函数。
  • 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函 数,它才能成为非抽象类。

函数模板

  • 假如我们要写一个swap函数,交换两个变量的值,如果要交换两个int,我们要写一个swap(int a, int b),如果要换两个double,又要重载一下,如果还有其他类型,就还需要重载很多次。这样做比较麻烦,对于这些比较相似的函数,我们可以直接使用函数模板来实现。

  • 编译器由模板生成函数的过程称为模板的实例化,

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <class T>
    void Swap(T & x,T & y) {
    T tmp = x;
    x = y;
    y = tmp;
    }

    int main() {
    int n = 1,m = 2;
    Swap(n,m); //编译器自动生成 void Swap(int & ,int & )函数
    double f = 1.2,g = 2.3;
    Swap(f,g); //编译器自动生成 void Swap(double & ,double & )函数
    return 0;
    }