学习笔记 C++ 类与对象

类与对象

学了之后 感觉类与结构体其实差不太多

1.声明类

class Human
{
	string Name;//内部变量
	int Age,Birth;
	
	void Talk(string Text);//封装函数
	void IntroduceSelf();
};

注:属于类的函数被称为方法

2.实例化对象

就像结构体一样 运行程序时使用类的话也需要实例化

class Human
{
    //...
};

Human person;

3.访问类的成员

访问类的成员也类似于结构体 存在句点访问和指针运算符(->)访问

class Human
{
    public:
      string Name;
      int Age,Birth;
	
      void Talk(string Text);
      void IntroduceSelf();
};
Human person;
Human* people;

int main()
{
    cout<<person.Age<<endl;
    people->Talk("string");
    return 0;
}

4.关键字private和public

定义类的时候有的时候会这么定义

class Human
{
    private:
        string Name;//内部变量
        int Age,Birth;
    public:
        void Talk(string Text);//封装函数
        void IntroduceSelf();
};

诚如字面意思

private下面定义的变量和函数只能在类内部调用

public下面定义的变量和函数可以在外部调用

就像这样

捕获.PNG

这样的优点是:可以通过合适的方法暴露内部变量 从而防止外部输入对内部设置造成破坏

这也体现了C++作为面向对象的程序设计语言的特点:C++让类的设计者能够控制类属性的访问与操作方式

5.关键字private实现数据抽象

有的时候类内部的数据不能直接公开

所以 类内部使用private定义数据 加以抽象化输出 实际上就是外部无法直接访问 合法的访问又经过了内部加工

class Human
{
    private:
      string Name;//内部变量
      int Age,Birth;
    public:
      int GetAge()
      {
	if(Age>30) cout<<Age-2<<endl;//唯一的合法访问经过了内部加工
	else cout<<Age<<endl;
      }  
};

6.构造函数

构造函数是一种特殊的函数 ta与类同名且不返回任何值

内部声明如下:

class Human
{
    public:
      Human()
      {
          //... 
      }
};

外部声明如下:

class Human
{
    public:
      Human();
};
Human::Human()
{

}

构造函数总是在创建对象的时候调用 是类成员变量初始化为已知值的理想场所

比如说:

class Human
{
    private:
      string Name;
      int Age;
    public:
      Human()
      {
        Name="WangXiaoMing";
      	Age=20;
      }
};

同样 构造函数支持重载 可创建一个默认构造函数和多个包含不同形参的重载构造函数

#include<bits/stdc++.h>
using namespace std;
class Human
{
    private:
      string Name;
      int Age;
    public:
      Human()
      {
      	Name="WangXiaoMing";
      	Age=20;
      }
      Human(string InputName)
      {
	Name=InputName;
	Age=38;
      }
	Human(string InputName,int InputAge)
      {
	Name=InputName;
        Age=InputAge;
      }
      void IntroduceSelf()
      {
        cout<<Name<<" "<<Age<<endl;
      }
};

int main()
{
  Human cdy;
  Human wzy("GaoXiaoHong");
  Human zjz("YuXiaoLiang",17);
  cdy.IntroduceSelf();wzy.IntroduceSelf();zjz.IntroduceSelf();
  return 0;
}

//
Output:
WangXiaoMing 20
GaoXiaoHong 38
YuXiaoLiang 17
//

注意 如果一个函数只存在重载的构造函数而不存在默认的构造函数的话

如果我们还直接Human cdy;这样直接声明类的话是不合法的

必须要提供内部变量的默认值

可以在声明的时候提供 Human zjz("YuXiaoLiang",17);

也可以在定义构造函数的时候添加默认值

class Human
{
    private:
      string Name;
      int Age;
    public:
      Human(string InputName="Adam",int InputAge=20)//添加默认值
      {
	Name=InputName;
        Age=InputAge;
      }
      void IntroduceSelf()
      {
        cout<<Name<<" "<<Age<<endl;
      }
};

Human cdy;
Human zjz("YuXiaoLiang",17);
//这样声明都是合法的

我们籍此也能明白 默认构造函数是调用时可不提供参数的构造函数,不意味ta不接受任何参数

同时还存在一种写法就是初始化列表

class Human
{
    private:
      string Name;
      int Age;
    public:
      Human(string InputName="Adam",int InputAge=20):Name(InputName),Age(InputAge){}
      void IntroduceSelf()
      {
        cout<<Name<<" "<<Age<<endl;
      }
};

Human cdy;
Human zjz("YuXiaoLiang",17);
//这样声明都是合法的

7.析构函数

与构造函数一样 析构函数也是一个看起来与类同名的函数 但是前面有一个波浪号~

class Human
{
  ~Human();
};

析构函数同构造函数类似 既可以在类内部声明 也可以在类外部声明

析构函数的作用与构造函数完全相反

每当对象不再在作用域内或者通过delete删除继而被销毁时 都将调用析构函数

析构函数是重置变量以及释放动态内存和其他资源的理想场所

我们使用C风格char* s;的字符串时 必须自己管理内存分配 因此建议使用std::string等工具

后者都是类 而且充分利用了构造函数和析构函数

接下来演示一下如何在构造函数中为一个字符串分配内存并在析构函数中释放

#include<bits/stdc++.h>
using namespace std;
class MyString
{
  private:
    char* buffer;
  public:
    MyString(const char* InitialString)
    {
      if(InitialString!=NULL)
      {
        buffer=new char[strlen(InitialString)+1];
        strcpy(buffer,InitialString);	
      }
      else buffer=NULL;	
    }	
    ~MyString()
    {//析构函数
      printf("Now we delete.\n");	
      if(buffer!=NULL) delete[] buffer;	
    }
    int getlen(){return strlen(buffer);}
    const char* getstring(){return buffer;}
};
int main()
{
  MyString person("Say Hello To The World");
  printf("It has %d characters.\n",person.getlen());
  printf("buffer contains:%s.\n",person.getstring());
  return 0;
} 
//
Output:
It has 22 characters.
buffer contains:Say Hello To The World.
Now we delete.
//

8.复制构造函数

我们调用函数的时候double Area(double InputRadius)实参会被复制给形参InputRadius 这种规则也适用于对象

上一程序当中 MyString类包含了一个指针类型成员 指向动态分配的内存

复制这个类的时候 我们会复制指针 但是不会复制指针指向的缓冲区 也就是导致两个对象指向同一块动态分配的内存

我们称之为浅复制 这会威胁程序的稳定性

比如说下面这个程序

#include<bits/stdc++.h>
using namespace std;
class MyString
{
  private:
    char* buffer;
  public:
    MyString(const char* InitialString)
    {
      if(InitialString!=NULL)
      {
      buffer=new char[strlen(InitialString)+1];
      strcpy(buffer,InitialString);	
      }
      else buffer=NULL;	
    }	
    ~MyString()
    {
      printf("Now we delete.\n");	
      if(buffer!=NULL) delete[] buffer;	
    }
    int getlen(){return strlen(buffer);}
    const char* getstring(){return buffer;}
};
void UseMyString(MyString person)
{
  printf("It has %d characters.\n",person.getlen());
  printf("buffer contains:%s.\n",person.getstring());
  return;
}
int main()
{	
  MyString person("Say Hello To The World");
  UseMyString(person);
  return 0;
} 

运行结果如下:

捕获.PNG

如果你可以看懂Dev-C++的exe的话 没错RE(runtime error)了

C++中使用delete释放内存块或者指针内存之后 不要进行访问 不能多次释放

由于形参复制实参之后 指针指向的是同一内存 所以void UseMyString(MyString person)在运行结束之后析构函数调用delete释放了这块内存 而主函数运行结束之后析构函数对于同一块内存又释放了一次 所以导致RE

一般来讲编译器会默认进行深复制
但是这里没有 因为编译时ta不确定MyString::buffer指向的是多少字节的内存

所以 我们使用复制构造函数进行深复制

复制构造函数也存在内部声明与外部声明

大概长成这个模样

class MyString
{
  MyString(const MyString& CopySource)
  {	
  }
};
MyString::MyString(const MyString& CopySource)
{
}

具体如下:

#include<bits/stdc++.h>
using namespace std;
class MyString
{
  private:
    char* buffer;
  public:
  MyString(const char* InitialString)
  {
    if(InitialString!=NULL)
    {
      buffer=new char[strlen(InitialString)+1];
      strcpy(buffer,InitialString);	
      cout<<"buffer points to 0x"<<hex<<(unsigned int*)buffer<<endl;
    }
    else buffer=NULL;
  }	
  MyString(const MyString& CopySource)
  {//复制构造函数
    printf("Now we are copying\n");
    if(CopySource.buffer!=NULL)
    {
      buffer=new char[strlen(CopySource.buffer)+1];
      strcpy(buffer,CopySource.buffer);	
      cout<<"buffer points to 0x"<<hex<<(unsigned int*)buffer<<endl;
    }
    else buffer=NULL;
  }
  ~MyString()
  {
    printf("Now we delete.\n");	
    if(buffer!=NULL) delete[] buffer;	
  }
  int getlen(){return strlen(buffer);}
  const char* getstring(){return buffer;}
};
void UseMyString(MyString person)
{
  printf("It has %d characters.\n",person.getlen());
  printf("buffer contains:%s.\n",person.getstring());
  return;
}
int main()
{	
  MyString person("Say Hello To The World");
  UseMyString(person);
  return 0;
} 

复制构造函数可以保证函数调用的时候神采用深复制

但是 当通过复制进行复制时 由于我们没有指定任何赋值运算符 赋值仍然采用浅复制 所以我们还需要制定赋值运算符

MyString::operator =(const MyString& CopySource){}

复制构造函数调用形参时 添加const可防止源对象被修改 同时复制构造函数的参数必须按引用传递 否则调用时将复制形参的值 导致浅复制——这正是我们要极力避免的

所以 类包含原始指针成员(char*等)时,务必编写复制构造函数和复制复制运算符

为了减少工作量 务必将类成员声明为std::string和智能指针类,而非原始指针,因为它们自己实现了复制构造函数

9.结构不同于类的地方

关键字struct来自于C 在C++来看与类及其相似 差别在于程序员未指定时默认的访问限定符(private与public)不同

除非指定了 否则结构中的成员默认为公有的

除非指定了 否则结构以公有方式继承基结构

10.声明友元

对于类中private定义的私有数据成员和方法 我们无法从外部访问 但是这不适用于友元类和友元函数

我们通过关键字friend声明友元 从而使得友元类和友元函数可以访问私有数据成员和方法

#include<bits/stdc++.h>
using namespace std;
class Human
{
  private:
  string Name;
  friend class Unity;//声明友元类
  friend void Display(const Human& person);//声明友元函数
  public:
  Human(string InputName="Adam"):Name(InputName){}
};
class Unity
{
  public:
  static void DisplayAge(const Human& person)
  {cout<<person.Name<<endl;}
};
void Display(const Human& person)
{
  cout<<person.Name<<endl;
}
int main()
{	
  Human cdy;
  Display(cdy);
  Unity::DisplayAge(cdy);
  return 0;
} 

11.静态数据类型

类的数据成员和函数成员 可以分为两类静态与非静态

通过在类型名之前添加static 可以使静态成为非静态

类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。

非静态成员需要在类实例化之后才能使用 而静态成员既可以通过实例化使用 也可以不经过实例化使用

也就是 静态成员与类相联系 不与类的对象相联系

class Student
{
  public:
    static int allcoount;
    int cardnumber;
};
int main()
{
  Student s1;
  Student::allcoount;//未经实例化便可以使用
  s1.allcoount;//经过实例化也可以使用 
  s1.cardnumber;//只有经过实例化才可以使用 
  return 0;
} 

静态成员数据

使用的时候必须要初始化 并且是在类中声明 在类外初始化
【为什么static数据成员一定要在类外初始化?】

#include<bits/stdc++.h>
using namespace std;
class Student
{
  public:
    static int allcoount;
};
int Student::allcoount=0;
int main()
{
  for(int i=1;i<=10;++i) Student::allcoount++;
  printf("%d\n",Student::allcoount);
  return 0;
}  

在实际运行的时候 静态成员数据为所有类共享 ta可以被所有的类对象修改 也可以被所有的类对象访问

#include<bits/stdc++.h>
using namespace std;
class Student
{
  public:
    static int allcount;
  Student(){allcount++;}
};
int Student::allcount=0;
int main()
{
  for(int i=1;i<=10;++i)
  {
    Student* now=new Student;
    cout<<now->allcount<<endl;	
  }
  cout<<Student::allcount; 
  return 0;
} 
/*
Output:
1
2
3
4
5
6
7
8
9
10
10
*/

静态成员函数

使用类似于静态成员数据 既可以实例化 也可以不实例化

#include<bits/stdc++.h>
using namespace std;
class Student
{
  public:
    static void print(){printf("Hello Word!\n");}
};
int main()
{
  Student s;
  Student::print();
  s.print();
  return 0;
} 

上文我们已经说过了 静态成员在非静态成员之前已经与全局变量一起被分配内存

一个静态成员函数不与任何对象相联系 不能对非静态成员进行默认访问

#include<bits/stdc++.h>
using namespace std;
class Student
{
  private:
    string name;
  public:
    Student()
    {name="ewkjhkjsd";}
    static string findname()
    {return name;}//这是错误的 因为不知道这里的name隶属于哪个对象?
};
int main()
{
  Student ss;
  cout<<ss.findname()<<endl;
  return 0;
} 

若要访问非静态成员时,必须通过参数传递的方式得到相应的对象,再通过对象来访问


Q:静态成员函数与非静态成员函数的根本区别是什么?

A:根本区别在于静态成员函数没有this指针 而非静态成员函数有一个指向当前对象的this指针

一般来讲 非静态成员函数会将当前的对象的地址作为第一个传递参数 传递的形参就是 this指针

在函数内部 非静态成员函数在访问非静态成员时都会自动将this参数作为指向当前对象的指针

而静态成员函数被调用 没有任何对象的地址被传递

因此 当访问非静态成员时 无this指针 会出错

这也是为什么一个静态成员函数会与当前任何对象都无联系

12.继承

1).含义

类中的一些东西 我觉得很好 希望其它类中也有 这样就有了继承这个概念

继承 顾名思义 就是一个类从另一个类中继承了部分数据成员和方法 继承的那个类叫做派生类 被继承的类叫做基类

class A //基类
{};
class B:public A  //派生类
{};

2).派生类对于基类的调用

派生类可以直接使用基类中定义的部分数据成员和方法

但是要注意的是 由于构造函数和析构函数不是虚函数 所以基类的构造函数和派生函数无法被派生类继承

同时 具体的调用的话 和派生类的继承方式以及基类中定义的成员和方法的类型有关

首先 类中定义的数据成员和方法有三种类型: private,protected,public三p党

private:只有在类中函数以及友元函数可以调用

protected:只有在类中函数,友元函数以及继承的派生类中函数可以调用

public:可以在任意情况下调用

而继承方式也有三种 私有继承(private),公有继承(public),保护继承(protected)

class A
{};
class B:public A //公有继承
{};
class C:private A //私有继承
{};
class D:protected A //保护继承
{};

而在派生类中访问基类成员的权限则是继承方式与基类成员类型共同作用的结果

1.公有继承 public
①基类中为public 派生类中仍为protected
②基类中为protected 派生类中仍为protected
③基类中为private 派生类中无法访问

2.保护继承 protected
①基类中为public 派生类中为protected
②基类中为protected 派生类中仍为protected
③基类中为private 派生类中无法访问

3.私有继承 private
①基类中为public 派生类中为private
②基类中为protected 派生类中为private
③基类中为private 派生类中无法访问


源自我的课本

保护继承和私有继承这种代码重用方式更多地考虑到代码安全 初学者很少涉及
①公共继承,将使基类作为开源代码不断让类程序员派生代码从而迅速传播
②保护继承,将使基类作为公司的内部技术不断接续和派生
③私有继承,将使技术世代单传,开发者在其派生类中也只能通过调用基类的方法来访问基类


3).派生类的构造函数与析构函数

【借鉴的博客】

构造函数

派生类无法继承基类的构造函数 所以只能在派生类中自定义构造函数 而如果要初始化基类的数据成员 必须调用基类的构造函数

接下来是简单的调用

#include<bits/stdc++.h>
using namespace std;
class cdy
{
  protected:
    int a,b;
  public:
    cdy(int aa,int bb):a(aa),b(bb){}	
    void print(){printf("a=%d b=%d\n",a,b);}
};
class wzy:public cdy
{
  protected:
    int c;
  public:
    wzy(int aa,int bb,int cc):cdy(aa,bb),c(cc){}
    void print(){printf("a=%d b=%d c=%d\n",a,b,c);}
};
class zjz:public wzy
{
  protected:
    int d,e;
  public:
    zjz(int aa,int bb,int cc,int dd,int ee):wzy(aa,bb,cc),d(dd),e(ee){}
    void print(){printf("a=%d b=%d c=%d d=%d e=%d\n",a,b,c,d,e);}	
};
int main()
{
  cdy x(100,140);
  wzy y(100,250,38);
  zjz z(238,23,4,75,123);
  x.print();
  y.print();
  z.print();
  return 0;
}

调用结果如下:
捕获.PNG

如果派生类中包含了基类子对象作为其成员

#include<bits/stdc++.h>
using namespace std;
class cdy
{
  protected:
    int a,b;
  public:
    cdy(int aa,int bb):a(aa),b(bb){}	
    int needa(){return a;}
    int needb(){return b;}
    void print(){printf("a=%d b=%d\n",a,b);}
};
class wzy:public cdy
{
  protected:
    int c;
    cdy d;
  public:
    wzy(int aa,int bb,int cc,int dd,int ee):cdy(aa,bb),c(cc),d(dd,ee){}
    void print(){printf("a=%d b=%d c=%d d.a=%d d.b=%d\n",a,b,c,d.needa(),d.needb());}
};
int main()
{
  cdy x(100,140);
  wzy y(238,23,4,75,123);
  x.print();
  y.print();
  return 0;
}

这是调用结果

捕获.PNG

要注意的是


在调用派生类构造函数之前 系统会先调用基类的构造函数
如果派生类构造函数中含有对基类子对象的初始化 那么基类子对象也会先调用自己的构造函数来初始化自身
最后才是派生类调用自己的构造函数来初始化自身新增成员


如果基类无构造函数或者基类构造函数无参(无法传递参数)
那么会调用基类默认的构造函数来初始化


基类中重载了构造函数 则对于基类成员初始化时构造函数具体调用会视参数使用个数的状况而定

析构函数

这里的话我没有整理太多

只需要明白 关于继承 构造函数先实现调用基类构造函数再调用派生类构造函数 而析构函数则先实现调用派生类析构函数再调用基类析构函数

4)派生类对于访问控制的调整

在派生类中 可以调整成员的访问控制属性 类如将基类的公有成员调整为私有成员 保护成员调整成公有成员 .etc

class Base
{
  private:
    int b1;
  protected:
    int b2;
    void fb2(){b1=1;}
  public:
    int b3;
    void fb3(){b1=1;}	
};
class Pri:private Base//私有继承导致基类全部成员为私有或者不可见 
{
  public:
    using Base::b3;//调整基类成员b3为公有 

};
int main()
{
  Pri pri;
  pri.b3=1;// ok
}

前提:调整对象必须是在派生类中课件 例如例子代码基类中的b1作为基类私有成员 在派生类中不可见 无法进行调整

5)多继承

有的类可以作为多个基类的派生类

class Base1
{
  protected:
    int id;
  public:
    Base1():id(0){}
    void Base1print(){printf("now is Base1.\n");}
    void changeid(int x){id=x;}
};
class Base2
{
  protected:
    int id;
  public:
    Base2():id(0){}
    void Base2print(){printf("now is Base2.\n");}
    void changeid(int x){id=x;}	
};
class Pri:public Base1,public Base2//私有继承导致基类全部成员为私有或者不可见 
{
  public:
    Pri(){};
    void Priprint(){printf("now is Pri.\n");}
};
int main()
{
  Pri pri;
  pri.Base1print();
  pri.Base2print();
  pri.Priprint();

  return 0;
}

输出结果:

now is Base1.
now is Base2.
now is Pri

然而 我们发现 当两个基类中出现相同的成员时(例如例子代码中的id) 会导致直接访问时冲突

这就是多继承的模糊性

我们必须要说明基类 否则会编译错误

pri.Base1::changeid(2);
pri.Base2::changeid(3); 

6)虚拟继承

按上述的例子 我们可以发现 其实我们可以将这些基类的共同属性再汇总到一个类中去

class Base
{
  protected:
    int id;
  public:
  	
    Base():id(0){}
    void Baseprint(){printf("now is Base.\n");}
    void changeid(int x){id=x;}
};
class Base1:public Base
{
  public:
    Base1(){}
    void Base1print(){printf("now is Base1.\n");}
};
class Base2:public Base
{
  public:
    Base2(){}
    void Base2print(){printf("now is Base2.\n");}
};
class Pri:public Base1,public Base2
{
  public:
    Pri(){};
    void Priprint(){printf("now is Pri.\n");}
};
int main()
{
  Pri pri;
  pri.changeid(3);//编译出错
  return 0;
}

为什么会编译出错

因为changeid(int)函数很明显是Base里的 但是当派生类Pri调用的时候 不确定其属于Base1的继承部分还是Base2的继承部分

这样的会导致指针出错

为了只有一个Base拷贝 为引入了虚拟继承

class Base
{
  protected:
    int id;
  public:
  	
    Base():id(0){}
    void Baseprint(){printf("now is Base.\n");}
    void changeid(int x){id=x;}
};
class Base1:virtual public Base//虚拟继承
{
  public:
    Base1(){}
    void Base1print(){printf("now is Base1.\n");}
};
class Base2:virtual ppublic Base//虚拟继承
{
  public:
    Base2(){}
    void Base2print(){printf("now is Base2.\n");}
};
class Pri:public Base1,public Base2
{
  public:
    Pri(){};
    void Priprint(){printf("now is Pri.\n");}
};
int main()
{
  Pri pri;
  pri.changeid(3);
  return 0;
}

virtual关键字的意思:如果没有Base拷贝 就添加一个 否则就使用已有的那一个

这将使得应用程序main()对于引用公共部分不再模糊

7)多继承与组合构造顺序

虚拟优先 然后非虚拟 都是虚拟或者非虚拟的话按照继承的顺序来

与构造函数的书写无关

class Base1
{
  public:
    Base1(){printf("now is Base1.\n");}
};
class Base2
{
  public:
    Base2(){printf("now is Base2.\n");}
};
class Base3
{
  public:
    Base3(){printf("now is Base3.\n");}
};
class Base4
{
  public:
    Base4(){printf("now is Base4.\n");}
};
class Object1
{
  public:
    Object1(){printf("now is Object1.\n");}
};
class Object2
{
  public:
    Object2(){printf("now is Object2.\n");}
};
class Now:public Base1,virtual public Base2,public Base3,virtual public Base4
{
  protected:
    Object1 ob1;
    Object2 ob2;		
  public:
    Now():Base4(),Base3(),Base2(),Base1(){printf("now is Now.\n");}		
};
int main()
{
  Now now;
  return 0;
}

Output

now is Base2.
now is Base4.
now is Base1.
now is Base3.
now is Object1.
now is Object2.
now is Now.


这些基本上是目前整理的 我可能还会不定期更新的

posted @ 2021-03-01 17:30  tcswuzb  阅读(186)  评论(0编辑  收藏  举报