C++类与对象详解

什么是类和对象

类和对象的概念

类是对象的抽象,对象是对客观事物的抽象。

用通俗的话来说:

类是类别的意思,是数据类型。
对象是类别下的具体事物。

也就是说:

类是数据类型,对象是变量。

比如:

水果是类。(水果不是真实存在的)
一个苹果是具体的对象。(一个苹果是真实存在的,它有大小,有颜色)

类的定义

定义一个类:盒子Box

class Box{
   public:
      double length;	// 盒子的长度
      double breadth;	// 盒子的宽度
      double height;	// 盒子的高度
};

解释1:

class是一个专门用于定义类的关键字。
Box为类名
public下面会讲到,这里不必在意。
double length是类的数据成员,这里一共定义了三个数据成员。分别代表Box的长宽高。
将类的成员使用大括号{}包裹,记得最后使用分号结束

解释2:

盒子是有自己的属性的,比如说盒子的长宽高,盒子的颜色和材质(上面的代码没有定义)。
我们称这些属性为类的数据成员

尽管有数据成员,类是没有存储空间的,只是一段代码存储在内存中。(因为类是抽象的,不是真实存在的)
只有实例化为对象,才会创建内存空间供数据成员存储。
也就是说,每一个盒子都会有自己的大小。
比如,int是没有存储空间的,但是int i=0;就会为变量i申请4字节的内存空间。

对象的创建
class Box{
   public:
      double length;	// 盒子的长度
      double breadth;	// 盒子的宽度
      double height;	// 盒子的高度
};
Box box1;               //使用类创建对象box1
box1.breadth = 20;      //将对象box1的数据成员breadth的值修改为20
box1.height = 20;
box1.length = 20;

//得到box1的数据成员的值,再得到盒子box1的体积
cout << "盒子的体积为" << box1.breadth* box1.height*box1.length << endl;    

解释:

Box为类名,是数据类型。
box1为变量名,是对象。它是通过类Box实例化的一个对象。
可以看到类就像是一个模板,对象是根据这个模板创建的。
box1.breadth表示访问对象中的数据成员breadth。
访问数据成员的语法:对象名.成员名。

思考:如果我们使用动态内存分配来创建对象,那么如果访问数据成员。

分析:我们一般是通过【对象名.成员名】。如果使用动态内存分配,那么我们就没有对象名了,只有指针名。

class Box{
   public:
      double length;	// 盒子的长度
      double breadth;	// 盒子的宽度
      double height;	// 盒子的高度
};
Box *p = new Box;       //使用动态内存分配的方式创建对象
p->breadth = 20;        //使用指针修改对象的数据成员
p->height = 20;
p->length = 20;

//使用指针得到对象的数据成员的值,在得到盒子的体积
cout << "盒子的体积为" << p->breadth * p->height * p->length << endl

成员函数

成员函数的定义
class Box{
   public:
      double length;	// 长度
      double breadth;	// 宽度
      double height;	// 高度
   
      double getVolume(){   //成员函数
         return length * breadth * height;
      }
};

解释:

成员函数定义在类中,可以访问对象中的所有成员(包括数据成员和成员函数)。
(有些数据成员是不能被随便访问的,public的作用就是限制访问的,后面会讲到)

成员函数没有内存空间:

数据成员在每个对象中都有内存空间,但是成员函数在存在类中,在内存中只是一段代码,需要被对象使用时,则被调用。

成员函数的使用
class Box{
   public:
      double length;	// 长度
      double breadth;	// 宽度
      double height;	// 高度
      double getVolume(){
         return length * breadth * height;
      }
};
Box box1;
box1.breadth = 20;
box1.height = 20;
box1.length = 20;

//调用成员函数得到对象的体积
cout << "盒子的体积为" << box1.getVolume() << endl;

解释:

成员函数的调用方法与数据成员一致,通过【对象名.函数名(参数)】调用。
函数可访问此对象的所有成员。

成员函数声明

普通函数中,可以先声明再调用最后定义。
在类的成员函数中,同样可以进行函数声明。

class Box{
   public:
      double length;	// 长度
      double breadth;	// 宽度
      double height;	// 高度
      double getVolume();   //成员函数声明
};
double Box::getVolume(){    //在类的外面进行函数定义
    return length * breadth * height;
}

解释:

使用【类名::】来修饰函数,表示此函数是Box的成员函数。(其他类可能也有同名的函数)
在调用时,是使用【对象名.函数名(参数)】调用的。

为什么要这么做?

因为函数具有封装性,我们只要关心函数的使用,不必关心函数是如何实现的。


类的访问修饰符

数据封装是面向对象编程的一个重要特点,类的访问修饰符防止直接访问类的内部成员。
类成员的访问限制是通过标记public、private、protected来指定的。
关键字public、private、protected称为访问修饰符

比如:

class Base {
   public:
  // 这里是公有成员
 
   protected:
  // 这里是受保护成员
 
   private:
  // 这里是私有成员
};
1.公有成员-public

公有成员在程序中类的外部是可访问的。也就是说,谁都可以访问这些公有成员,没有任何限制。

class Box{
   public:
        double length;	// 盒子的长度
        double breadth;	// 盒子的宽度
        double height;	// 盒子的高度
};
void main(){
        Box box1;
        box1.breadth = 20;
}

我们发现:可以直接通过【对象名.成员名】访问对象的成员。
相对的,我们可以看一下private和protected是都能通过这种方式直接访问对象的成员。

2.私有成员-private

私有成员在类的外部是不可访问的,甚至是不可查看的。
也就是说,在类的外部是不可以通过【对象名.成员名】访问对象的成员的。
类的私有成员只能在类的内部被直接调用。

class Box
{
private:
   double length;	// 盒子的长度
   double breadth;	// 盒子的宽度
   double height;	// 盒子的高度
};
void main(){
   Box box1;
// box1.breadth = 20;	//这行代码会报错!
}

解释:

在以上的代码中,由于breadth是私有成员,所以直接通过【对象名.数据成员】的形式访问会报错。


类的私有成员是默认的成员,如果没有写访问修饰符,则默认为private。

比如:

//下面的数据成员都是private
class Box{
   double length;
   double breadth;
   double height;
};

通过函数间接访问private:

如果我们在类的外部需要访问私有成员,我们可以通过函数间接访问
原理:

1.private不能被直接访问.
2.public可以被直接访问。
3.类的内部可以随意调用任意成员。
得到结论:通过public成员间接调用private成员。

xclass Box
{
private:
   double length;	// 盒子的长度
   double breadth;	// 盒子的宽度
   double height;	// 盒子的高度
public:
   void setBreadth(double B){
            Breadth = B;
   }
   double getBreadth(){
            return breadth;
   }
};
void main(){
   Box box1;
   box1.setBreadth(12.5);
   cout <<"盒子的宽度为: "<< box1.getBreadth() << endl;
}

解释:

length、breadth和height都是私有成员。
setBreadth(double B)getBreadth()是公有成员。
我们通过这两个set和get函数修改和得到breadth的值。
通过这样的方式,我们就可以访问私有成员了。

3.受保护成员-protected

保护成员访问控制与私有成员十分相似。在类的外部不可被直接访问。
但有一点不同,保护成员在派生类(即子类)中是可访问(以后再说)。

UTOOLS1576408252981.png

构造函数和析构函数

1.构造函数

什么是构造函数?

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
而且这个构造函数时自动执行的,无需手动调用。

构造函数有什么作用?

构造函数可用于初始化,例如:为某些成员变量设置初始值。

如何定义构造函数?

  1. 构造函数的名称与类的名称是完全相同。
  2. 不会返回任何类型,也不会返回 void。
class Box{
private:
   double length;	// 盒子的长度
   double breadth;	// 盒子的宽度
   double height;	// 盒子的高度
public:
   Box(double L,double B,double H){	//构造函数
      length = L;  breadth = B;  height = H;
   }
   double getBreadth(){
      return breadth;
   }
};
void main(){
   Box box1 = Box(12, 12, 12);	//定义的时候初始化
   cout << "盒子的宽度为: " << box1.getBreadth() << endl;
}
2.默认构造函数
  1. 类中带有默认构造函数,若没有定义构造函数,则会采用默认构造函数创建对象。
  2. 默认构造函数是没有参数的,不会初始化数据成员。(就是空的函数,什么事也不做)。
  3. 若自定义了构造函数,则C++不会为我们创建默认构造函数。
    我们可以进行构造函数重载,使得初始化多样化。

例1:

Box(){      //默认构造函数,由程序自动创建
}
//如果我们自定义了其他函数,则程序不会为我们自动创建构造函数

例2:

#include<iostream>
using namespace std;
class Box{
private:
	double length;	// 盒子的长度
	double breadth;	// 盒子的宽度
	double height;	// 盒子的高度
};
void main(){
	Box box1=Box();   //此处不会报错,因为自动创建了构造函数Box(){}
}

例3:

#include<iostream>
using namespace std;
class Box{
private:
	double length;	// 盒子的长度
	double breadth;	// 盒子的宽度
	double height;	// 盒子的高度
public:
	Box(double L, double B, double H){	//构造函数
		length = L;  breadth = B;  height = H;
	}
};
void main(){
	Box box1;      //报错!!!!不存在默认构造函数
}
/*
解释:
   因为我们自己创建了一个构造函数,所以程序不会为我们创建默认的构造函数Box(){}
   如果想要有默认的构造函数:可以使用函数重载,自己创建Box(){}
*/

例4:

#include<iostream>
using namespace std;
class Box
{
private:
	double length;	// 盒子的长度
	double breadth;	// 盒子的宽度
	double height;	// 盒子的高度
public:
	Box(double L, double B, double H){	//构造函数
		length = L;  breadth = B;  height = H;
	}
	Box(){      //构造函数
	}
};
void main(){
	Box box1 = Box(11,12,14);
	Box box2;	//相当于Box box2=Box();
}
3.析构函数

什么是析构函数?

类的析构函数是类的一种特殊的成员函数,它会在每次删除对象时执行。

如何定义析构函数?

1.析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀。
2.它不会返回任何值,也不能带有任何参数。

析构函数有什么作用?

析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
比如:在构造函数中使用了动态内存分配,那么系统并不会自动回收这些内存。所以需要析构函数。

如何调用析构函数?

无需手动调用,在程序运行完后自动调用析构函数,完成内存释放和文件关闭等功能。(所以只要关心如何定义即可)

class Arr{
public:
   int *p;
   Arr(){		//构造函数
      p = new int[5];
   }
   ~Arr(){	//析构函数
      delete[] p;
   }
};
/*
解释:
   析构函数作用其中之一就是回收动态内存分配的空间。
   new和delete要成对出现。
   将delete写在析构函数中,就可以在销毁对象时自动回收内存了。
   析构函数还有其他的作用:比如关闭文件。
*/
4.拷贝构造函数

什么是拷贝构造函数?

拷贝构造函数是一种特殊的构造函数,它在创建对象时,可以用对象给对象赋值。

拷贝构造函数有什么作用?

如果对象中有指针,那么使用对象给对象赋值后,它们的指针将会指向同一块内存。这样它们就会产生关联,而不是两个不同的对象。

如何定义拷贝构造函数?

类名 (const 类名 &obj) {
   //省略构造函数的主体
}

例1:

/*
这个例子没有使用拷贝构造函数,但是运行结果是正常的。
例子中:使用了对象给对象赋值。
对象给对象赋值的过程中,会将所有成员变量的值复制一遍。
*/
#include<iostream>
using namespace std;
class Box
{
public:
	double length;	// 盒子的长度
	double breadth;	// 盒子的宽度
	double height;	// 盒子的高度
	Box(double L, double B, double H){	//构造函数
		length = L;  breadth = B;  height = H;
	}
};
void main(){
	Box box1 = Box(1, 1, 1);
	Box box2 = box1;
	cout << box2.length << endl;     //输出1
}
/*
解释:
   这个例子中:将box1的值复制一份给box2。
*/

例2:

#include<iostream>
using namespace std;
class Arr{
public:
	int *p;
	Arr(){      //构造函数
		p = new int[5];   //动态内存分配5个int的空间
	}
	~Arr(){     //析构函数
		delete[] p;       //回收动态内存分配的空间
	}
};
void main(){
	Arr a;
	a.p[1] = 1;    //将a中的p[1]修改为1
	Arr b = a;
	b.p[1] = 2;    //将b中的p[1]修改为2
	cout << a.p[1] << endl;    //输出2!!!
	cout << b.p[1] << endl;    //输出2!!!
	getchar(); getchar(); getchar(); getchar(); getchar();
}
/*
现象:
   我们将b中的p[1]修改为2,但是a中的p[1]也被修改为了2.
   我们并没有修改a中的p[1],为什么也变成了2?
解释:
   Arr b = a;将a的值赋值给了b。
   那么,a的值就和b一样了,即他们的数据成员p指向的内存空间是同一个。
   所以,修改b的值,a的值也会跟着变化。
*/
UTOOLS1576414358568.png

产生的问题:

我们想要创建的是两个不同的对象,但是上面这个例子:两个对象相互关联了。

如何解决:

拷贝构造函数

#include<iostream>
using namespace std;
class Arr{
public:
	int *p;
	Arr(){
		p = new int[5];
	}
	~Arr(){
		delete[] p;
	}
	Arr(const Arr&obj){		//拷贝构造函数
		p = new int[5];
		for (int i = 0; i < 5; i++)
			p[i] = obj.p[i];
	}
};
void main(){
	Arr a;
	a.p[1] = 1;
	Arr b = a;
	b.p[1] = 2;
	cout << a.p[1] << endl;    //输出1
	cout << b.p[1] << endl;    //输出2
	getchar(); getchar(); getchar(); getchar(); getchar();
}
/*
解释:
   有了拷贝构造函数,对象给对象赋值的时候就不会只复制地址了,而是复制地址所在的内存空间的值。
*/

友元

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。

尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数
友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

使用关键字friend来定义友元类和友元函数

友元函数举例
class Box
{
   double width;
public:
   friend void printWidth(Box box);
   void setWidth(double wid){
      width = wid;
   }
};
void printWidth(Box box){
   cout << box.width << endl;
}

void main(){
   Box b;
   b.setWidth(10);
   printWidth(b);	//外部函数访问private成员
}
友元类举例
class Box
{
   double width;
public:
   friend class friendBox;
      void setWidth(double wid){
      width = wid;
   }
};
class friendBox{
public:
   void testFunc(Box box){
      cout << "box.width = " << box.width << endl;
   }
};
void main(){
   Box b;
   b.setWidth(10);
   friendBox f;
   f.testFunc(b);		//友元类中的所有函数都是友元函数
}

内联函数

背景:

函数在调用的时候会发生转移,会在栈中保存现场(比如原来的变量值),这样的操作会花费一定的内存空间和时间。

内联函数:

在调用时不发生控制转移,而是在编译时将函数体嵌入在每一个调用处。
适用于功能简单,规模较小又使用频繁的函数。
好处:加快程序运行效率

注意:

  1. 递归函数无法内联处理,内联函数不能有循环体,switch语句,不能进行异常接口声明。
  2. 即便将函数声明为内联函数,程序在编译时若发现不适合作为内敛函数,则按普通函数处理。
  3. 类中的所有成员函数都是内联函数。
inline int Max(int x, int y){	//将函数声明为内联函数
   return (x > y)? x : y;
}
int main(){
   cout << "Max (20,10): " << Max(20,10) << endl;
   return 0;
}

this指针

每一个对象都有自己的地址,this指向自己。
可以通过this来访问本对象的成员。

class Box{
public:
   double length; 	// 长度
   double breadth;     // 宽度
   double height;    	// 高度
   double setLength(double length){
      this->length=length;	//通过this指针访问自身的数据成员
   }
};
int main(){
   Box box1;
   box1.setLength(10);	//长度被修改为10
   cout<<box1.length<<endl;
   return 0;
}

解释:

细心发现,函数double setLength(double length)中的length和数据成员中的额length冲突了,函数参数length比数据成员length的作用域更小,所以默认访问的是函数参数length。
那么我们如何去访问数据成员length呢?
因为this指针永远指向自己(本对象),所以可以通过this->length来访问。

静态成员

静态成员在类的所有对象中是共享的。无论创建多少对象,静态成员始终只有一个。
我们使用static关键字将成员声明为静态。
静态成员分为静态数据成员静态成员函数

1.静态数据成员

静态数据成员需要在类中定义,在类外初始化。

访问方式:

类名::变量名(推荐)
对象名.变量名。

例1-静态成员是共享的:

class A{
public:
   static int i;  //静态数据成员定义
};
int A::i=0;       //静态数据成员初始化
int main(){
   A a1,a2;
   a1.i=1;
   cout<<a2.i<<endl;    //输出结果为1
   return 0;
}

解释:

我们将a1的i修改为1,可以发现a2的i也变成了1,所以静态成员是共享的。

例2-静态数据成员的作用举例:

class A{
public:
    static int ObjectCount;         //静态成员
    A(){
        ObjectCount++;	//每创建一个对象,此变量加1(用于记录对象的数量)
    }
   ~A(){
      ObjectCount--;	//每回收一个对象,此变量减1
   }
};
int A::ObjectCount=0;               //静态成员初始化
void main(){
   A a1;
   A a2;
   cout<<A::ObjectCount<<endl;
}

解释:

静态数据成员是共享的。在构造函数中,将此变量+1;在析构函数中,将此变量-1。
那么,这个成员就是用来记录对象的数量。(当前存在多少个A的对象)

1.静态数据成员

静态成员函数只能访问静态成员(包括数据成员和静态成员函数)和类外部其他函数
静态成员函数没有this指针。

解释:

因为静态成员函数只有一份,那么如果他要访问对象中的数据成员,该访问哪一个对象呢?所以,不能让静态成员函数访问对象中的非静态成员。
this指针是每个对象都有的,指向本对象。静态成员是类拥有的,而不是对象,所以类没有this指针,静态成员函数也就不能使用this指针了。

访问方式:

类名::静态函数名(推荐)
对象名.静态函数名

举例:

class A{
public:
    static int ObjectCount;         //静态成员
   A(){
      ObjectCount++;
   }
   ~A(){
      ObjectCount--;	//每回收一个对象,此变量减1
   }
   static void print(){           //静态成员函数
      cout<<ObjectCount<<endl;    //访问静态成员
   }
};
int A::ObjectCount=0;               //静态成员初始化
void main(){
   A a;
   A::print();
}











posted @ 2019-12-28 16:50  NetRookieX  阅读(12)  评论(0编辑  收藏  举报