• Coursera课程笔记----C++程序设计----Week3


    类和对象(Week 3)

    内联成员函数和重载成员函数

    内联成员函数

    • inline + 成员函数
    • 整个函数题出现在类定义内部
    class B{
      inline void func1(); //方式1
      void func2() //方式2
      {
        
      };
    };
    
    void B::func1(){}
    

    成员函数的重载及参数缺省

    • 重载成员函数
    • 成员函数——带缺省参数
    #include<iostream>
    using namespace std;
    class Location{
      private:
      	intx,y;
      public:
      	void init(int x=0,int y=0); //存在2个缺省参数
      	void valueX(int val) {x = val;}//1
      	int valueX(){return x;}//2
      //1和2是重载函数
    }
    
    • 使用缺省参数要注意避免有函数重载时的二义性

    构造函数

    基本概念

    • 成员函数的一种
      • 名字与类名相同,可以有参数,不能有返回值(void也不行)
      • 作用时对对象进行初始化,如给成员变量赋初值
      • 如果定义类的时候没写构造函数,则编译器生成一个默认的无参数的构造函数
        • 默认构造函数无参数,无任何操作
      • 如果定义了构造函数,则编译器不生成磨人的无参数的构造函数
      • 对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
      • 一个类可以有多个构造函数
    • 为什么需要构造函数
      1. 执行必要初始化工作,不需要专门再写初始化函数
      2. 有时对象没被初始化就使用,会导致程序出错
    class Complex{
      private:
      	double real,imgae;
      public:
      	void Set(double r, double i);
    };//编译器自动生成默认的构造函数
    
    Complex c1;//默认的构造函数被调用
    Complex* pc = new Complex;//默认的构造函数被调用
    
    
    class Complex{
      private:
      	double real,imgae;
      public:
      	Complex(double r, double i = 0);
    };
    Complex::Complex(double r, double i){
      real = r;
      imag = i;
    }
    
    Complex c1; //error,缺少构造函数的参数
    Complex *pc = new Complex;//error,没有参数
    Complex c1(2); //OK
    Complex c1(2,4),c2(3,5);
    Complex *pc = new Complex(3,4);
    
    • 可以有多个构造函数,参数个数或类型不同
    class Complex{
      private:
      	double real,imgae;
      public:
      	void Set(double r, double i);
      	Complex(double r,double i);
      	Complex(double r);
      	Complex(Complex c1,Complexc2);
    };
    
    Complex::Complex(double r, double i)
    {
      real = r;imag = i;
    }
    
    Complex::Complex(double r)
    {
      real = r; imag = 0;
    }
    Complex::Complex(Complex c1, Complex c2)
    {
      real = c1.real+c2.real;
      imag = c1.imag+c2.imag;
    }
    Complex c1(3),c2(1,0),c3(c1,c2);
    //c1={3,0} c2={1,0} c3={4,0};
    

    构造函数在数组中的使用

    class CSample{
      	int x;
      public:
      	CSample(){
          cout<<"Constructor 1 Called"<<endl;
        }
      	CSample(int n){
          x = n;
          cout<<"Constructor 2 Called"<<endl;
        }
    };
    
    int main()
    {
      CSample array1[2]; // 1 1
      cout<<"step1"<<endl;
      CSample array2[2] = {4,5};//2 2
      cout<<"step2"<<endl;
      CSample array3[2] = {3};//2 1
      cout<<"step3"<<endl;
      CSample *array4 = new CSample[2];//1 1
      delete []array4; // 收回空间
      return 0;
    }
    
    class Test{
      public:
      	Test(int n){ }//(1)
      	Test(int n, int m){ }//(2)
      	Test(){ }//(3)
    };
    Test array1[3] = {1,Test(1,2)} //三个元素分别用(1),(2),(3)进行初始化
    Test array2[3] = {Test(2,3),Test(1,2),1};//2 2 1
    Test * pArray[3] = {new Test(4), new Test(1,2)};//1 2 ❌
    

    复制构造函数(copy constructor)

    • 基本概念
      • 只有一个参数,即对同类对象的引用
      • 形如 X::X( X&)或X::X(const X &),二选一,后者能以常量对象作为参数
      • 如果没有定义复制构造函数,那么编译器生成默认复制构造函数,默认的复制构造函数完成复制功能
      • 如果定义了复制构造函数,默认的复制构造函数将不存在。
    class Complex{
      private:
      	double real,imag;
    };
    Complex c1;//调用缺省无参构造函数
    Complex c2(c1);//调用缺省的复制构造函数,将c2初始化成和c1一样
    
    class Complex{
      public:
      	double real,imag;
      Complex(){ }
      Complex(const Complex & c){
        real = c.real;
        imag = c.imag;
        cout<<"Copy Constructor called";
      }
    };
    Complex c1;
    Complex c2(c1);
    
    • 注意
      • 不允许有形如X::X(X)的构造函数
    class CSample{
      CSample(CSample c){
        //error,不允许这样的构造函数
      }
    }
    
    • 复制构造函数起作用的三种情况

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

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

        class A
        {
          public:
          A(){ };
          A(A & a){
            cout<<"Copy constructor called"<<endl;
          }
        }
        
        void Func(A a1){ }
        int main(){
          A a2;
          Func(a2);
          return 0;
        }
        
      3. 如果函数的返回值时类A的对象时,则函数返回时,A的复制构造函数将被调用

    • 为什么要自己写复制构造函数

      • 后续会讲解

    类型转换构造函数

    • 目的
      • 实现类型的自动转换
    • 特点
      • 只有一个参数
      • 不是复制构造函数
    • 编译系统会自动调用➡️转换构造函数➡️建立一个临时对象/临时变量
    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;
        }
    };
    
    iint main()
    {
      Complex c1(7,8);//对c1进行初始化,调用传统构造函数
      Complex c2 = 12; //对c2进行初始化,调用类型转换构造函数,不会生成一个临时对象
      c1 = 9; // 赋值语句,虽然赋值号两边类型不同,但是编译器没有报错。
      //编译器以9作为实参,调用类型转换构造函数,9被自动转换成一个临时Complex对象,赋值给c1
    }
    

    析构函数(Destructor)

    • 回顾:构造函数
      • 成员函数的一种
      • 名字与类名相同
      • 可以有参数,不能有返回值
      • 可以有多个构造函数
      • 用来初始化对象
    • 析构函数
      • 成员函数的一种
      • 名字与类名相同(在函数名之前加'~')
      • 无参数,无返回值
      • 一个类最多只能有一个析构函数
      • 在对象消亡时,自动被调用
        • 在对象消亡前做善后工作
          • 释放分配的空间等
      • 定义类时没写析构函数,则编译器生成缺省析构函数
        • 不涉及释放用户申请的内存释放等清理工作
      • 定义了析构函数,则编译器不生成缺省析构函数
    class String{
      private:
      	char *p;
      public:
      String(){
        p = new char[10];
      }
      ~String();
    };
    
    String::~String(){
      delete [] p;
    }
    
    • 析构函数和数组
      • 对象数组生命周期结束时,对象数组的每个元素的析构函数都会被调用
    • 析构函数和运算符delete
      • delete运算导致析构函数调用
    • 例题总结
      • 先被构造的对象,最后被析构掉(平级的情况下)
      • 构造函数和析构函数在不同编译器中
        • 个别调用情况不一致
          • 编译器有bug
          • 代码优化措施
        • 课程中讨论的是C++标准的规定,不牵扯编译器的问题

    静态成员变量和静态成员函数

    基本概念

    • 静态成员:在说明前面+static关键字的成员,有静态成员变量和静态成员函数
    • 普通成员变量每个对象有各自的一份,而静态成员变量一共一份,所有对象共享
    • sizeof运算符不会计算静态成员变量
    • 普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象
    • 静态成员不需要通过对象就能访问
    • 静态成员变量本质上是全局变量
    • 静态成员函数本质上是全局函数
    • 设置静态成员这种机制,目的是将和某些类紧密相关的全局变量和函数写进类中,看上去像一个整体,易于维护和理解

    如何访问静态成员

    1. 类名::成员名
    2. 对象名.成员名
    3. 指针->成员名
    4. 引用.成员名

    静态成员示例

    • 考虑一个需要随时知道矩形总数和总面积的图形处理程序
    • 可以用全局变量来记录总数和总面积(造成变量和类之间的关系不直观,且变量能够被其他类访问,存在一定风险)
    • 用静态成员将这两个变量封装进类中,更容易理解和维护
    • 必须在定义类的文件中对静态成员变量进行一次说明or初始化,否则编译能通过,链接不能通过

    注意事项

    • 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数
    • 考虑到复制构造函数的影响

    成员对象和封闭类

    成员对象

    • 一个类的成员变量是另一个类的对象
    • 包含成员对象的类叫封闭类(Enclosing)
    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};
    };
    CCar::CCar(int p, int tr,int w):price(p),tyre(tr,w){ };
    
    int main()
    {
      CCar car(20000,17,225);
      return 0;
    }
    
    • 如果CCar类不定义构造函数,则:

      • CCar car;//error➡️编译错误
        
      • 编译器不知道car.type该如何初始化

      • car.engine的初始化没有问题,可以用默认构造函数

    • 生成封闭类对象的语句➡️明确“对象中的成员对象”➡️如何初始化

    封闭类构造函数的初始化列表

    • 定义封闭类的构造函数时,添加初始化列表

      • 类名::构造函数(参数表):成员变量1(参数表),成员变量2(参数表),...

        {

        ......

        }

      • 成员对象初始化列表中的参数

        • 任意复杂的表达式
        • 函数/变量/表达式中的函数,变量有定义

    调用顺序

    • 当封闭类对象生成
      • 执行所有成员对象的构造函数
      • 执行封闭类的构造函数
    • 成员对象的构造函数调用顺序
      • 和成员对象在类中的说明顺序一致
      • 与在成员初始化列表中出现的顺序无关
    • 当封闭类对象消亡
      • 执行封闭类的析构函数
      • 执行成员对象的析构函数
    • 先构造的后析构,后构造的先析构

    友元

    友元函数

    • 一个类的友元函数可以访问该类的私有成员
    class CCar; // 提前声明CCar类,以便后面CDriver类使用
    class CDriver{
      public:
      void ModifyCar(CCar* pCar); //改装汽车
    };
    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;
    }
    
    • 可以将一个类的成员函数(包括构造,析构函数)定义成另一个类的友元
    class B{
      public:
      	void function();
    };
    
    class A{
      friend void B::function();
    };
    

    友元类

    • A是B的友元类➡️A的成员函数可以访问B的私有成员
      • 友元类之间的关系,不能传递,不能继承
    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;}
    

    this指针

    this指针作用

    • 指向成员函数所作用的对象
    • 非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针
    class Complex{
      public:
      	double real,imag;
      	void Print(){
          cout<<real<<","<<imag;
        }
      Complex(double r, double i):real(r),imag(i){ }
      Complex AddOne(){
        this->real++; //=real++
        this->Print();//=Print()
        return *this;
      }
    };
    
    int main()
    {
      Complex c1(1,1),c2(0,0);
      c2 = c1.AddOne();
      return 0;
    }
    
    class A{
      int i;
      public:
      	void Hello(){cout<<"hello"<<endl;}
    };//编译器把该成员函数编译成机器指令后,会变成
    //void Hello(A *this){cout<<"hello"<<endl;}
    //如果Hello函数变成 void Hello(){cout<<i<<"hello"<<endl;}
    //就会出错
    
    int main()
    {
      A *p = NULL;
      p->Hello(); //结果会怎样?
    }//输出:hello
    //编译器把该成员函数编译成机器指令后,会变成
    //hello(p)
    

    注意事项

    • 静态成员函数中不能使用this指针
    • 因为静态成员函数并不具体作用于某个对象
    • 因此,静态成员函数的真实的参数的个数,就是程序中写出的参数的个数

    常量对象、常量成员函数和常引用

    常量对象

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

    常量成员函数

    • 在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数
    • 常量成员函数执行期间不应修改其所作用的对象。因此,在常量成员函数中不能修改成员变量的值(静态变量除外),也不能调用同类的非常量成员函数(静态成员函数除外)
    class Sample
    {
      public:
      	int value;
      	void GetValue() const;
      	void func(){};
      	Sample(){}
    };
    void Sample::GetValue() const
    {
      value = 0;//wrong
      func();//wrong
    }
    
    int main(){
      const Sample o;
      o.value = 100; //err,常量对象不可以被修改
      o.func();//err常量对象上面不能执行非常量成员函数
      o.GetValue();//ok
      return 0;
    }
    

    常量成员函数的重载

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

    常引用

    • 引用前面可以加const关键字,成为常引用。不能通过常引用,修改其引用的变量
    const int & r = n;
    r = 5;//error
    n = 4;//ok
    
    • 对象作为函数的参数时,生成该参数需要调用复制构造函数,效率比较低。用指针做参数,会让代码的可读性变差
    • 所以可以用对象的引用作为参数。
    • 但对象引用作为参数有一定的风险,若函数中不小心修改了形参,则实参也会跟着改变,如何避免?
    • 所以可以用对象的常引用作为参数
    • 这样函数中就能确保不会出现无意中更改形参值的语句了

    练习题

    注:填空题在Coursera提交时,文件中只需出现填进去的内容即可

    Quiz 1

    #include<iostream>
    #include<stdio.h>
    #include<cstring>
    #include<string>
    #include<string.h>
    using namespace std;
    class A {
    public:
        int val;
        A (int n = 0){val = n;}
        A & GetObj(){
            return *this;
        }
    };
    int main() {
        A a;
        cout << a.val << endl;
        a.GetObj() = 5;
        cout << a.val << endl;
    }
    

    Quiz 2

    #include<iostream>
    #include<stdio.h>
    #include<cstring>
    #include<string>
    #include<string.h>
    using namespace std;
    class Sample{
    public:
        int v;
        Sample(int n):v(n) { }
        Sample(const Sample &a)
        {
            v = a.v*2;
        }
    };
    int main() {
        Sample a(5);
        Sample b = a;
        cout << b.v;
        return 0;
    }
    

    Quiz 3

    #include<iostream>
    #include<stdio.h>
    #include<cstring>
    #include<string>
    #include<string.h>
    using namespace std;
    class Base {
    public:
        int k;
        Base(int n):k(n) { }
    };
    class Big {
    public:
        int v;
        Base b;
        Big(int n = 5):v(n),b(n){ };
    };
    int main() {
        Big a1(5); Big a2 = a1;
        cout << a1.v << "," << a1.b.k << endl;
        cout << a2.v << "," << a2.b.k << endl;
        return 0;
    }
    

    Quiz 4 魔兽世界之一:备战

    #include <iostream>
    #include <cstdio>
    #include <string>
    using namespace std;
    const int WARRIOR_NUM = 5;
    /*
    string Warrior::names[WARRIOR_NUM] = { "dragon","ninja","iceman","lion","wolf" };
    红方司令部按照 iceman、lion、wolf、ninja、dragon 的顺序制造武士。
    蓝方司令部按照 lion、dragon、ninja、iceman、wolf 的顺序制造武士。
    */
    
    class Headquarter;
    class Warrior
    {
    private:
        Headquarter * pHeadquarter; //指向英雄所属阵营的指针
        int kindNo;//武士的种类编号 0 dragon 1 ninja 2 iceman 3 lion 4 wolf
        int no;//英雄编号
    public:
        static string names[WARRIOR_NUM]; //存放5种职业名字的数组
        static int initialLifeValue [WARRIOR_NUM]; //存放不同英雄的起始生命值(从输入中采集)
        Warrior( Headquarter *p,int no_,int kindNo_);//构造函数
        void PrintResult(int nTime); //执行打印数据的工作,若无法继续创建则输出结束并停止
    };
    
    class Headquarter
    {
    private:
        int totalLifeValue;
        bool stopped;
        int totalWarriorNum;
        int color;
        int curMakingSeqIdx; //当前要制造的武士是制造序列中的第几个
        int warriorNum[WARRIOR_NUM]; //存放每种武士的数量
        Warrior * pWarriors[1000];//和每个创建的英雄建立链接
    public:
        friend class Warrior;
        static int makingSeq[2][WARRIOR_NUM];//武士的制作序列,按阵营的不同分成两个
        void Init(int color_, int lv); //初始化阵营需要颜色和总血量
        ~Headquarter();
        int Produce(int nTime); //创建英雄,输入时间
        string GetColor();
    };
    
    Warrior::Warrior(Headquarter *p, int no_, int kindNo_) {
        no = no_;
        kindNo = kindNo_;
        pHeadquarter = p;
    }
    
    void Warrior::PrintResult(int nTime) {
        string color = pHeadquarter->GetColor();
        printf("%03d %s %s %d born with strength %d,%d %s in %s headquarter
    ",
                nTime, color.c_str(), names[kindNo].c_str(),no,initialLifeValue[kindNo],
                pHeadquarter->warriorNum[kindNo],names[kindNo].c_str(),color.c_str()); // string 在printf中输出的函数调用c_str()
    }
    
    void Headquarter::Init(int color_, int lv) {
        color = color_;
        totalLifeValue = lv;
        totalWarriorNum = 0;
        stopped = false;
        curMakingSeqIdx = 0;
        for (int i = 0; i < WARRIOR_NUM; i++) {
            warriorNum[i] = 0;
        }
    }
    
    Headquarter::~Headquarter() {
        for (int i = 0; i < totalWarriorNum; i++) {
            delete pWarriors[i];
        }
    }
    
    int Headquarter::Produce(int nTime) {
        if(stopped)
            return 0;
        int searchingTimes = 0;
        while(Warrior::initialLifeValue[makingSeq[color][curMakingSeqIdx]] > totalLifeValue &&
                searchingTimes < WARRIOR_NUM)
        {
            curMakingSeqIdx = (curMakingSeqIdx + 1) % WARRIOR_NUM;
            searchingTimes++;
        }
        int kindNo = makingSeq[color][curMakingSeqIdx];
        if(Warrior::initialLifeValue[kindNo] > totalLifeValue)
        {
            stopped = true;
            if(color == 0)
                printf("%03d red headquarter stops making warriors
    ",nTime);
            else
                printf("%03d blue headquarter stops making warriors
    ",nTime);
            return 0;
        }
        //排除所有其他条件后,开始制作士兵
        totalLifeValue -= Warrior::initialLifeValue[kindNo];
        curMakingSeqIdx =( curMakingSeqIdx + 1) % WARRIOR_NUM;
        pWarriors[totalWarriorNum] = new Warrior(this,totalWarriorNum+1,kindNo);
        warriorNum[kindNo]++;
        pWarriors[totalWarriorNum]->PrintResult(nTime);
        totalWarriorNum++;
        return 1;
    }
    
    string Headquarter::GetColor() {
        if(color == 0)
            return "red";
        else
            return "blue";
    }
    
    string Warrior::names[WARRIOR_NUM] = {"dragon","ninja","iceman","lion","wolf"};
    int Warrior::initialLifeValue[WARRIOR_NUM];
    int Headquarter::makingSeq[2][WARRIOR_NUM]={{2,3,4,1,0},{3,0,1,2,4}};//两个司令部武士的制作顺序序列
    
    int main()
    {
        int t;
        int m;
        Headquarter RedHead,BlueHead;
        scanf("%d", &t); //读取case数
        int nCaseNo = 1;
        while(t--){
            printf("Case:%d
    ",nCaseNo++);
            scanf("%d",&m);//读取基地总血量
            for (int i = 0; i < WARRIOR_NUM; i++) {
                scanf("%d",&Warrior::initialLifeValue[i]);
            }
            RedHead.Init(0,m);
            BlueHead.Init(1,m);
            int nTime = 0;
            while (true){
                int tmp1 = RedHead.Produce(nTime);
                int tmp2 = BlueHead.Produce(nTime);
                if( tmp1 == 0 && tmp2 == 0)
                    break;
                nTime++;
            }
        }
        return 0;
    }
    //老师给的答案读了好几遍,大概捋顺了……
    //现阶段自己根本写不出来这种程序,在第一步抽象出两个类这块就感觉很困难
    //慢慢加油吧……
    
  • 相关阅读:
    在Vue构建的SPA网页里 刷新的话,显示404页面
    springboot2.x 设置404页面
    关于Typora不显示PicGo.app的问题
    DBeaver中table插入新的数据
    DBeaver修改table的column名字
    Zeal
    Android Studio 快速创建 Toast
    使用VSCode调试单个JavaScript文件
    使用maven打包普通的java项目
    在命令行界面实现彩色字符输出 -- 并且介绍和这个相关的比较好用的java类库
  • 原文地址:https://www.cnblogs.com/maimai-d/p/12894078.html
Copyright © 2020-2023  润新知